- 2026/08/23 Sun
-
TweetVault④
前回
TweetVault①
TweetVault②
TweetVault③絞り込み UI の改善
テキストボックスにタグを入力する形で複数条件の絞り込みに対応したけど、タグ一覧から選択するようにしたい。- Ctrl を押しながら選択した場合は複数選択にする。
- 選択済みの場合は解除。
- 後述の除外選択中のタグを Ctrl で選択した場合は状態を上書き。
- Alt を押しながら選択した場合は除外するタグの選択として扱う
- こちらも複数選択可。
- 選択済みの場合は解除。
- 通常選択中のタグを Alt で選択した場合は状態を上書き。
- 通常選択のハイライトが青なのに対して、こちらは赤にする。
- Ctrl も Alt も押下していない場合は従前の通り排他選択。
- 複数選択や除外選択されているタグがある場合は解除する。
frontend/src/app/page.tsx@@ -17,3 +17,3 @@ const [bookmarks, setBookmarks] = useState<Bookmark[]>([]); - const [selectedTag, setSelectedTag] = useState<string>(); + const [selectedTag, setSelectedTag] = useState({inc:[], exc:[]}); const [searchQuery, setSearchQuery] = useState(''); @@ -54,3 +54,3 @@ const data = await api.getBookmarks({ - tag: selectedTag, + tag: JSON.stringify(selectedTag), search: searchQuery, @@ -117,4 +117,26 @@ - function handleTagSelect(tag: string | undefined) { - setSelectedTag(tag); + function handleTagSelect(tag: string | undefined, e?: any) { + if (!tag) { + setSelectedTag({inc:[], exc:[]}); + return; + } + setSelectedTag(prev => { + if (e && e.ctrlKey) { + return { + inc: prev.inc.includes(tag) + ? prev.inc.filter(item => item !== tag) + : [...prev.inc, tag], + exc: prev.exc.filter(item => item !== tag) + }; + } else if (e && e.altKey) { + return { + exc: prev.exc.includes(tag) + ? prev.exc.filter(item => item !== tag) + : [...prev.exc, tag], + inc: prev.inc.filter(item => item !== tag) + }; + } else { + return {inc:[tag], exc:[]}; + } + }); setCurrentPage(1);選択中のタグを保持する selectedTag を、文字列から対象/対象外リストを保持するオブジェクトに変更。
Ctrl/Alt 押下時の処理を追加。frontend/src/components/bookmarks/SearchAndFilter.tsx@@ -8,3 +8,3 @@ onTagSelect: (tag: string | undefined) => void; - selectedTag?: string; + selectedTag?: {}; onDeleteTag?: (tagName: string) => void; @@ -88,3 +88,3 @@ // If the deleted tag was selected, reset to show all bookmarks - if (selectedTag === tagName) { + if (selectedTag.inc.includes(tagName) || selectedTag.exc.includes(tagName)) { onTagSelect(undefined); @@ -122,3 +122,3 @@ className={`rounded-full px-3 py-1 text-sm whitespace-nowrap ${ - !selectedTag + selectedTag.inc.length == 0 && selectedTag.exc.length == 0 ? 'bg-blue-600 text-white' @@ -132,6 +132,6 @@ <div - onClick={() => onTagSelect(tag.name)} + onClick={(e) => onTagSelect(tag.name, e)} className={`px-3 py-1 text-sm whitespace-nowrap flex items-center cursor-pointer ${ - selectedTag === tag.name - ? 'bg-blue-600 text-white' + selectedTag.inc.includes(tag.name) ? 'bg-blue-600 text-white' : + selectedTag.exc.includes(tag.name) ? 'bg-red-600 text-white' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600'選択中タグに関する判定を修正。
キーボードイベントの引き渡しを追加。frontend/src/components/bookmarks/TagMenu.tsx@@ -8,3 +8,3 @@ onSuccess: () => void; - selectedTag?: string; + selectedTag?: {}; onDeleteTag?: (tagName: string) => void; @@ -134,3 +134,4 @@ className={`p-1 rounded-full cursor-pointer ${ - selectedTag === tag.name ? 'hover:bg-blue-700' : 'text-gray-500 hover:bg-gray-200' + selectedTag.inc.includes(tag.name) ? 'hover:bg-blue-700' : + selectedTag.exc.includes(tag.name) ? 'hover:bg-red-700' : 'text-gray-500 hover:bg-gray-200' }`}選択中タグに関する判定を修正。backend/internal/services/bookmark_service.go(前回からの差分)@@ -4,3 +4,2 @@ "encoding/json" - "fmt" "log" @@ -8,2 +7,3 @@ "strings" + "slices" @@ -197,6 +197,9 @@ - type Tag struct { + type RequestTag struct { + Include []string `json:"inc"` + Exclude []string `json:"exc"` + } + type QueryTag struct { Name string `json:"name"` } - var tags []Tag @@ -208,6 +211,24 @@ // Apply tag filter if provided - if tag == "untagged" { - query = query.Where("tags_json::jsonb = ?", "[]") - } else if tag != "" { - tags = append(tags, Tag{Name: tag}) + if tag != "" { + var req RequestTag + if err := json.Unmarshal([]byte(tag), &req); err != nil { + return nil, 0, err + } + if slices.Contains(req.Include, "untagged") { + query = query.Where("tags_json::jsonb = ?", "[]") + } else { + if len(req.Include) != 0 { + var tags []QueryTag + for _, v := range req.Include { + tags = append(tags, QueryTag{Name: v}) + } + tagsJson, _ := json.Marshal(tags) + query = query.Where("tags_json::jsonb @> ?", tagsJson) + } + for _, v := range req.Exclude { + tags := []QueryTag{{Name: v}} + tagsJson, _ := json.Marshal(tags) + query = query.Not("tags_json::jsonb @> ?", tagsJson) + } + } } @@ -218,18 +239,5 @@ for _, word := range words { - if strings.HasPrefix(word, "#") { - tags = append(tags, Tag{Name: strings.TrimPrefix(word, "#")}) - } else if strings.HasPrefix(word, "-#") { - query = query.Not("tags_json::jsonb @> ?", fmt.Sprintf(`[{"name":"%s"}]`, strings.TrimPrefix(word, "-#"))) - } else { - query = query.Where("full_text ILIKE ? OR name ILIKE ? OR screen_name ILIKE ?", - "%"+word+"%", "%"+word+"%", "%"+word+"%") - } - } - } - if len(tags) != 0 { - jsonData, err := json.Marshal(tags) - if err != nil { - return nil, 0, err + query = query.Where("full_text ILIKE ? OR name ILIKE ? OR screen_name ILIKE ?", + "%"+word+"%", "%"+word+"%", "%"+word+"%") } - query = query.Where("tags_json::jsonb @> ?", jsonData) }検索の実体。
テキストボックスの入力からタグ検索する処理は除去。複数キーワード対応は残す。
元の処理に合わせるためにいったんアンマーシャルしているのが微妙。
- 2026/08/22 Sat
-
胆のう摘出の手術をしてから二か月が経過。
術後の状態確認で MRI を撮ってくる。
結果は特に問題なし。
あと、今回の MRI でも膵臓には何も写りませんでした。
過去に診断された IPMN はなかったことになった。先生「他に何か気になることはありますか」
なや「腎臓で石が見つかっていたのが気になってるくらいですかね」
先生「(写真をスクロール)あーこれですね。これが下に落ちると…」
なや「めちゃくちゃ痛いやつですね」
先生「そう、めちゃくちゃ痛いやつww」
不意に「痛いやつ」って差し込まれてツボに入ったのか、先生爆笑してた。 スーパーの弁当コーナーでなんにしよと眺めてたら、店員さんに案内されてきたお婆さんが鮭ハラス飯を買っていき、うまそうだなと思ったので自分も購入。
帰ってから気付いたけど、これ二人前だな? 今日はかよちゃん。
土砂降りだったけど、雨雲レーダーを見た感じ2時間もすれば通り過ぎそう。
飲んでいるうちに上がるだろ。
足元びしょ濡れになるだろうから、靴下は脱いでサンダルで出掛ける。かよちゃん - 下総中山/居酒屋 | 食べログ
https://tabelog.com/chiba/A1202/A120202/12020652/

いつものように栄川を二合。
惣菜の盛り合わせ。

お次は木戸泉を二合。
今日は いかの丸干しがあったぞ。
女将さんが以前勤めていたスーパーの店長さんが今も昼営業に来てくれているって話は前に聞いていたけど、いつも定食と惣菜盛り合わせを食べながらビールに日本酒三合を飲んでいると聞いてびびる。
その量飲んで仕事戻ってるの。 雨はすっかり上がった。予定通り。
今日は雨上がりのにおいがしないなと思いつつ、江戸川沿いを歩く。
またサンダルで10km超歩いてる。
今日も元気。
- 2026/08/21 Fri
-
冷凍庫のロックアイスがでかいかたまりになってた。
一回溶けたなこれ。
開けっ放しになってたか?
前に買い替えたのが2009年なので、そろそろ故障してもおかしくはないんだよな。 歯医者20回目。
仮歯無限ループ。
- 2026/08/20 Thu
- 朝東京ばな奈。
- 2026/08/19 Wed
- 昨日と打って変わって忙しい。
- 2026/08/18 Tue
- 暇すぎて業務と関係ないコードずっと読んでる。
- 2026/08/17 Mon
-
朝自販機でお茶を買ったらSuicaの残額が15円。
LINE公式からメッセージがきててなんだろと思ったら、いつもLINEをご利用いただき、誠にありがとうございます。
FacebookアカウントによるLINEログイン機能のサポートは、2024年1月に終了しました。それに伴い、現在ご利用中のログイン方法では、ご利用いただけなくなります。
現在のLINEアカウントを引き続き利用するには、2026年12月31日までに電話番号を登録してください。
期日までに電話番号を登録されない場合、お客様のFacebook連携IDは削除され、LINEアカウントがご利用いただけなくなります。アカウントに紐づく情報やアイテムを継続してご利用いただくためには、必ず2026年12月31日までに電話番号の登録を完了してください。
Facebook連携を使って電話番号登録を回避して複数アカウントつくっていたのがついに無効になった。
- 2026/08/16 Sun
-
TweetVault③
前回
TweetVault①
TweetVault②絞り込み機能の拡張
標準だと「テキストボックスに入力した単一ワード」+「一覧から選択した単一タグ」の絞り込みにしか対応していないので、複数条件で絞り込めるようにしたい。- 複数ワードの絞り込みに対応
- テキストボックスに「#xxx」の形式で入力した場合にタグとして絞り込む。複数指定可。一覧で選択したタグとAND。
- テキストボックスに「-#xxx」の形式で入力した場合にタグを含まないものに絞り込む。複数指定可。
backend/internal/services/bookmark_service.go(前回からの差分)冷凍揚げナスで味噌汁。@@ -7,2 +7,3 @@ "strconv" + "strings" @@ -196,2 +197,7 @@ + type Tag struct { + Name string `json:"name"` + } + var tags []Tag + query := s.db.Model(&models.BookmarkView{}) @@ -205,3 +211,3 @@ } else if tag != "" { - query = query.Where("tags_json::jsonb @> ?", fmt.Sprintf(`[{"name":"%s"}]`, tag)) + tags = append(tags, Tag{Name: tag}) } @@ -210,4 +216,20 @@ if search != "" { - query = query.Where("full_text ILIKE ? OR name ILIKE ? OR screen_name ILIKE ?", - "%"+search+"%", "%"+search+"%", "%"+search+"%") + words := strings.Fields(search) + for _, word := range words { + if strings.HasPrefix(word, "#") { + tags = append(tags, Tag{Name: strings.TrimPrefix(word, "#")}) + } else if strings.HasPrefix(word, "-#") { + query = query.Not("tags_json::jsonb @> ?", fmt.Sprintf(`[{"name":"%s"}]`, strings.TrimPrefix(word, "-#"))) + } else { + query = query.Where("full_text ILIKE ? OR name ILIKE ? OR screen_name ILIKE ?", + "%"+word+"%", "%"+word+"%", "%"+word+"%") + } + } + } + if len(tags) != 0 { + jsonData, err := json.Marshal(tags) + if err != nil { + return nil, 0, err + } + query = query.Where("tags_json::jsonb @> ?", jsonData) }
- 2026/08/15 Sat
-
手術してから走れていなかったけど久しぶりに。
前日の雨で路面は悪く、ところどころ水浸しの状態。
カニも道路に迷い込んでた。
迷いガニオーバーラン。

サンダルで10km歩き回ってるおじさん。
- 2026/08/14 Fri
-
ブックマーク2500件の整理完了。
整理していた中で「元十徳の店員さんがやっている店」というのが発掘されて、そういえばブックマークしてたっけと思い出す。
元十徳の店員さんが手掛ける静岡料理の酒場🏮書いてたらまた行きたくなった_φ(・_・https://t.co/Ayn9f29dIT#酒場 #じゃんだら #中野坂上
— 路草 (@brutus_xxx) November 20, 2024
調べてみると、こちらの店のオープンは2012年。
自分が十徳に初めて行ったのは2014年なので自分が知っている店員さんではないか。
まぁせっかくなので行ってみようと一路中野坂上。
中野坂上に着いてみたところ既視感。
このときだ。
泥酔してスマホを紛失した翌朝、受け取りにいったのが中野坂上でした。。 お店は地上に出てすぐ。居酒屋 じゃんだら - 中野坂上/居酒屋 | 食べログ
https://tabelog.com/tokyo/A1319/A131903/13149071/

ブックマークしていた記事だと日本酒をいろいろ扱っていそうだったけど見当たらず。
松竹梅 豪快を注文。
静岡おでん。
大根にすじぼこ。
松竹梅をもう一杯。
変わり種でモッツァレラ巾着。立呑や じゃんだら(@janndara.nakanosakaue) • Instagram写真と動画
https://www.instagram.com/janndara.nakanosakaue/居酒屋 じゃんだら(@izakaya_jandara) • Instagram写真と動画
https://www.instagram.com/izakaya_jandara/
後で調べたら、インスタのアカウントが2つあり、下がリニューアル後のもの。
リニューアル前は日本酒をいろいろ扱っていたようだけど、変わってしまったのかな。 さくっと上がって次へ。
路地入ってすぐのところにL字カウンターのみの店があったのでネットで調べてみたけど情報が見つからず。
店名は福ちゃん。
新しい店というわけでもなさそうなんだけど。

店の外に貼ってあるメニューを見た感じ高いってわけでもないので入ってみる。

新宿 清酒。
アテに油揚げの納豆はさみ焼きを頼んだけどなかったので焼きさば。
福 純米吟醸。
入店時は自分一人だったけど、飲んでいるうちに一人二人とお姉さま方が。
ここも2杯でさくっと上がる。 移動して十徳へ。

壁のメニューから寿福絹子。
店員さんから「飲み方は?」と聞かれて一瞬「?」となったけど、これ焼酎か。日本酒じゃなかった。
まぁいいかとロックで注文。
アテにバクダン納豆。
さっき食べ損ねた納豆フラストレーションを解消。
続けて水鏡無私もロックで。
最後は日本酒で〆る。遊穂。
< Previous | Next >