Skip to content

[Feat] : 곡 카드에 title_ko / artist_ko 표시 (#197)#198

Merged
GulSam00 merged 3 commits into
developfrom
feat/197-showKoTranslation
Apr 19, 2026
Merged

[Feat] : 곡 카드에 title_ko / artist_ko 표시 (#197)#198
GulSam00 merged 3 commits into
developfrom
feat/197-showKoTranslation

Conversation

@GulSam00
Copy link
Copy Markdown
Owner

@GulSam00 GulSam00 commented Apr 19, 2026

📌 PR 제목

[Feat] : 곡 카드에 title_ko / artist_ko 표시

📌 변경 사항

  • GET /api/search 응답에 title_ko, artist_ko 필드 추가
  • Song 타입에 title_ko?, artist_ko? 옵셔널 필드 추가
  • 아래 카드에서 원어와 다른 경우에만 한국어 번역을 보조 라벨로 노출 (title 아래 / artist 아래, text-xs 스타일):
    • SearchResultCard (검색 결과)
    • SongCard (부를곡 목록)
    • RecentSongCard (최근 추가)

💬 추가 참고 사항

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@GulSam00
Copy link
Copy Markdown
Owner Author

/describe

@GulSam00
Copy link
Copy Markdown
Owner Author

/review

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
singcode Ready Ready Preview, Comment Apr 19, 2026 2:54pm

@GulSam00
Copy link
Copy Markdown
Owner Author

/improve

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 19, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Remediation recommended

1. Excess ResizeObservers 🐞 Bug ➹ Performance ⭐ New
Description
SearchResultCard/SongCard/RecentSongCard에서 title_ko/artist_ko를 MarqueeText로 추가 렌더링하여 카드당
MarqueeText가 최대 4개가 됩니다. MarqueeText는 인스턴스마다 ResizeObserver를 생성하므로(검색 페이지 size=20 기준) 관찰자/레이아웃 측정이
급증해 스크롤/렌더 성능이 저하될 수 있습니다.
Code

apps/web/src/app/search/SearchResultCard.tsx[R68-81]

+            <div className="flex w-[calc(100%-40px)] flex-col gap-0.5 truncate">
              <MarqueeText className="text-base font-medium">{title}</MarqueeText>
+              {title_ko && title_ko !== title && (
+                <MarqueeText className="text-muted-foreground text-xs">{title_ko}</MarqueeText>
+              )}
              <MarqueeText
-                className="text-muted-foreground hover:text-accent cursor-pointer text-sm hover:underline hover:underline-offset-4"
+                className="text-muted-foreground hover:text-accent mt-0.5 cursor-pointer text-sm hover:underline hover:underline-offset-4"
                onClick={onClickArtist}
              >
                {artist}
              </MarqueeText>
+              {artist_ko && artist_ko !== artist && (
+                <MarqueeText className="text-muted-foreground/70 text-xs">{artist_ko}</MarqueeText>
+              )}
Evidence
MarqueeText는 useEffect에서 매 인스턴스마다 ResizeObserver를 생성/observe 후 unmount 시 disconnect합니다. 이번 PR로 카드
컴포넌트들에 title_ko/artist_ko용 MarqueeText가 추가되어 리스트 렌더링 시 인스턴스 수가 크게 증가합니다. 또한 검색 API는 page size=20으로 한
페이지에 카드가 다수 렌더링되는 전제가 있어(=관찰자 수가 쉽게 수십~수백까지 증가) 성능 문제가 현실화될 수 있습니다.

apps/web/src/components/MarqueeText.tsx[13-56]
apps/web/src/app/search/SearchResultCard.tsx[65-82]
apps/web/src/app/api/search/route.ts[140-143]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
카드당 MarqueeText 인스턴스 증가로 ResizeObserver가 대량 생성되어 리스트(특히 검색 결과)에서 렌더/스크롤 성능 저하가 발생할 수 있습니다.

### Issue Context
`MarqueeText`는 인스턴스마다 `ResizeObserver`를 생성합니다. 이번 변경으로 title/title_ko/artist/artist_ko가 각각 MarqueeText로 렌더링되어 관찰자 수가 2배 가까이 증가합니다.

### Fix Focus Areas
- apps/web/src/components/MarqueeText.tsx[13-56]
- apps/web/src/app/search/SearchResultCard.tsx[65-82]
- apps/web/src/app/tosing/SongCard.tsx[34-46]
- apps/web/src/app/recent/RecentSongCard.tsx[13-23]

### Suggested fix direction
- 보조 라벨(title_ko/artist_ko)은 marquee가 꼭 필요하지 않다면 `truncate` + `title`(tooltip)로 단순 텍스트 처리해 ResizeObserver를 없애기
- 또는 `MarqueeText`에 옵트인 방식(예: `enableOverflowCheck`/`useResizeObserver` prop)을 추가해 보조 라벨에서는 observer를 비활성화하기
- 또는 리스트 뷰에서만 MarqueeText를 지연 렌더링/가시 영역에서만 동작하도록 최적화(가상 스크롤/IntersectionObserver 등)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Null translation contract 🐞 Bug ≡ Correctness
Description
GET /api/search always includes title_ko/artist_ko in the JSON response even when they are
unset, contradicting the Song type declaring them as optional strings. This can leak null/empty
values to clients and break code that relies on optional-field semantics (presence implies a real
string).
Code

apps/web/src/app/api/search/route.ts[R182-183]

+      title_ko: song.title_ko,
+      artist_ko: song.artist_ko,
Evidence
The API response builder unconditionally sets title_ko and artist_ko for every song object,
while the shared Song interface models these fields as optional (i.e., may be omitted).
Additionally, the translation cron logic treats these fields as frequently missing (it skips only
when both are truthy), implying many rows will have an “unset” value that will be forwarded as-is by
the API.

apps/web/src/app/api/search/route.ts[178-196]
apps/web/src/types/song.ts[1-14]
packages/crawling/src/cron/translationJpn.ts[31-38]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GET /api/search` always includes `title_ko`/`artist_ko` fields in the response object, even when there is no translation. This conflicts with `Song` typing these properties as optional and can propagate `null` values.
### Issue Context
- `Song` declares `title_ko?: string` / `artist_ko?: string`, which implies the property may be omitted.
- DB-originated values are often missing (translation job skips when these are already set), so the API should define a clear contract: either `string | null` OR omit the fields when missing.
### Fix Focus Areas
- apps/web/src/app/api/search/route.ts[178-196]
- apps/web/src/types/song.ts[1-14]
### Suggested fix
Choose one of:
1) **Omit when missing** (recommended for the current `?` typing):
- Map `null`/empty to `undefined` so `JSON.stringify` omits the keys:
- `title_ko: song.title_ko ?? undefined`
- `artist_ko: song.artist_ko ?? undefined`
2) **Allow null in the contract**:
- Change type to `title_ko?: string | null` and `artist_ko?: string | null` (and keep API as-is), making consumers explicitly handle null.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

3. Duplicated translation rendering 🐞 Bug ⚙ Maintainability ⭐ New
Description
title_ko/artist_ko 조건부 표시 로직이 SearchResultCard, SongCard, RecentSongCard에 동일하게 중복 구현되어 있습니다. 향후 노출
조건/스타일 변경 시 3곳을 동기화해야 해 누락으로 인한 UI 불일치 가능성이 커집니다.
Code

apps/web/src/app/tosing/SongCard.tsx[R37-45]

+          <div className="mb-1 flex w-[290px] flex-col gap-0.5">
            <MarqueeText className="text-base font-medium">{title}</MarqueeText>
+            {title_ko && title_ko !== title && (
+              <MarqueeText className="text-muted-foreground text-xs">{title_ko}</MarqueeText>
+            )}
            <MarqueeText className="text-muted-foreground text-sm">{artist}</MarqueeText>
+            {artist_ko && artist_ko !== artist && (
+              <MarqueeText className="text-muted-foreground/70 text-xs">{artist_ko}</MarqueeText>
+            )}
Evidence
세 카드 컴포넌트 모두에서 (koField && koField !== baseField) 조건과 보조 텍스트 스타일이 같은 패턴으로 반복되고 있습니다. 이런 중복은 사소한
요구사항 변경(예: 비교 로직 변경, 스타일 변경, 공백/정규화 처리 추가)에도 여러 파일을 수정해야 하므로 유지보수 비용과 누락 리스크를 높입니다.

apps/web/src/app/search/SearchResultCard.tsx[68-81]
apps/web/src/app/tosing/SongCard.tsx[37-45]
apps/web/src/app/recent/RecentSongCard.tsx[14-22]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
번역 라벨(title_ko/artist_ko) 렌더링 조건/스타일이 3개 카드 컴포넌트에 중복되어 있어 변경 시 누락으로 UI 불일치가 발생할 수 있습니다.

### Issue Context
현재 각 카드에서 동일한 조건(`ko && ko !== base`)과 유사한 className을 반복합니다.

### Fix Focus Areas
- apps/web/src/app/search/SearchResultCard.tsx[68-81]
- apps/web/src/app/tosing/SongCard.tsx[37-45]
- apps/web/src/app/recent/RecentSongCard.tsx[14-22]

### Suggested fix direction
- 예: `TranslatedSubLabel`(base, ko, className) 같은 공용 컴포넌트/유틸을 만들어 조건/스타일을 한 곳에서 관리
- 또는 `SongTitleBlock`처럼 title/artist(+ko) 묶음을 공통 컴포넌트로 추출해 카드들은 레이아웃만 책임지도록 분리

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Grey Divider

Previous review results

Review updated until commit 9ea9d4e

Results up to commit N/A


Grey Divider

New Review Started

This review has been superseded by a new analysis
Grey Divider Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Display Korean translations in search result cards

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add Korean translation fields to search API response
• Display Korean title/artist in search result cards conditionally
• Update Song type with optional title_ko and artist_ko fields
Diagram
flowchart LR
  API["GET /api/search API"] -- "adds title_ko, artist_ko fields" --> Response["Search Response"]
  Response -- "maps to Song type" --> SongType["Song Interface"]
  SongType -- "consumed by" --> Card["SearchResultCard Component"]
  Card -- "conditionally displays" --> KoTranslation["Korean translations below original text"]
Loading

Grey Divider

File Changes

1. apps/web/src/app/api/search/route.ts ✨ Enhancement +2/-0

Add Korean fields to search API response

• Map title_ko and artist_ko fields from database songs to API response
• Include Korean translation fields in SearchSong objects returned by GET endpoint

apps/web/src/app/api/search/route.ts


2. apps/web/src/types/song.ts ✨ Enhancement +3/-0

Add optional Korean translation fields to Song type

• Add optional title_ko? field to Song interface
• Add optional artist_ko? field to Song interface
• Support Korean translation metadata in song type definition

apps/web/src/types/song.ts


3. apps/web/src/app/search/SearchResultCard.tsx ✨ Enhancement +10/-4

Display Korean translations conditionally in card UI

• Destructure title_ko and artist_ko from song object
• Display Korean title below original title when different
• Display Korean artist below original artist when different
• Adjust spacing and styling for Korean translation labels

apps/web/src/app/search/SearchResultCard.tsx


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 19, 2026

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 19, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Remediation recommended

1. Null translation contract 🐞 Bug ≡ Correctness
Description
GET /api/search always includes title_ko/artist_ko in the JSON response even when they are
unset, contradicting the Song type declaring them as optional strings. This can leak null/empty
values to clients and break code that relies on optional-field semantics (presence implies a real
string).
Code

apps/web/src/app/api/search/route.ts[R182-183]

+      title_ko: song.title_ko,
+      artist_ko: song.artist_ko,
Evidence
The API response builder unconditionally sets title_ko and artist_ko for every song object,
while the shared Song interface models these fields as optional (i.e., may be omitted).
Additionally, the translation cron logic treats these fields as frequently missing (it skips only
when both are truthy), implying many rows will have an “unset” value that will be forwarded as-is by
the API.

apps/web/src/app/api/search/route.ts[178-196]
apps/web/src/types/song.ts[1-14]
packages/crawling/src/cron/translationJpn.ts[31-38]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`GET /api/search` always includes `title_ko`/`artist_ko` fields in the response object, even when there is no translation. This conflicts with `Song` typing these properties as optional and can propagate `null` values.

### Issue Context
- `Song` declares `title_ko?: string` / `artist_ko?: string`, which implies the property may be omitted.
- DB-originated values are often missing (translation job skips when these are already set), so the API should define a clear contract: either `string | null` OR omit the fields when missing.

### Fix Focus Areas
- apps/web/src/app/api/search/route.ts[178-196]
- apps/web/src/types/song.ts[1-14]

### Suggested fix
Choose one of:
1) **Omit when missing** (recommended for the current `?` typing):
- Map `null`/empty to `undefined` so `JSON.stringify` omits the keys:
 - `title_ko: song.title_ko ?? undefined`
 - `artist_ko: song.artist_ko ?? undefined`

2) **Allow null in the contract**:
- Change type to `title_ko?: string | null` and `artist_ko?: string | null` (and keep API as-is), making consumers explicitly handle null.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

PR Description updated to latest commit (624014e)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@GulSam00 GulSam00 changed the title [Feat] : 검색 결과 카드에 title_ko / artist_ko 표시 (#197) [Feat] : 곡 카드에 title_ko / artist_ko 표시 (#197) Apr 19, 2026
@GulSam00
Copy link
Copy Markdown
Owner Author

/review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 19, 2026

Persistent review updated to latest commit 9ea9d4e

@GulSam00 GulSam00 merged commit c9f3b9d into develop Apr 19, 2026
2 checks passed
@GulSam00 GulSam00 deleted the feat/197-showKoTranslation branch April 19, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 검색 결과 카드에 title_ko / artist_ko 표시

1 participant