Skip to content

[Feat] : 검색 단어 단위 AND 매칭 및 부분 일치 NULL 누락 버그 수정 (#254)#255

Merged
GulSam00 merged 2 commits into
developfrom
feat/254-searchSpaceHandling
Jun 11, 2026
Merged

[Feat] : 검색 단어 단위 AND 매칭 및 부분 일치 NULL 누락 버그 수정 (#254)#255
GulSam00 merged 2 commits into
developfrom
feat/254-searchSpaceHandling

Conversation

@GulSam00

Copy link
Copy Markdown
Owner

📌 PR 제목

[Feat] : 검색 단어 단위 AND 매칭 및 부분 일치 NULL 누락 버그 수정

📌 변경 사항

  • 검색어를 공백 기준 토큰으로 분리해, 입력한 모든 단어가 포함돼야 매칭(AND, 순서 무관) 하도록 api/search/route.ts 필터 재작성
    • 정확 티어: tokens.join('%')로 띄어쓰기 차이를 무시한 완전 일치 (예: 사건의 지평선사건의%지평선)
    • 부분 티어: 각 토큰의 "컬럼 OR 그룹"을 .or() 체이닝으로 AND 결합 (바톤 로드 검색 시 오버로드·로드넘버원 같은 단어 하나만 걸친 노이즈 제외)
  • 부분 일치 NULL 누락 버그 수정: title_ko/artist_ko가 null일 때 NOT ILIKENULL로 평가되어 행이 통째로 누락되던 문제를, col IS NULL을 OR로 묶어 NULL-safe하게 처리. (이전엔 title_ko가 채워진 J-pop만 부분 검색이 동작)
  • useSearchSong.ts: 한글 한정 공백 제거 로직 삭제 → 공백을 보존해 API로 전달 (토큰화/공백 처리는 API가 담당)
  • 가독성: 컬럼 OR 그룹 생성 로직을 buildColumnOrGroup 헬퍼로 추출 (DRY)

💬 추가 참고 사항

- 검색어를 공백 기준 토큰으로 분리해 모든 단어가 포함돼야 매칭(순서 무관)
- 정확 티어는 tokens.join('%')로 띄어쓰기 차이 무시 완전 일치
- title_ko/artist_ko가 null일 때 NOT ILIKE가 NULL로 평가돼 행이 누락되던 버그를 IS NULL OR로 수정
- useSearchSong: 한글 한정 공백 제거 로직 삭제(공백 보존, 토큰화는 API 담당)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@GulSam00

Copy link
Copy Markdown
Owner Author

/describe

@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
singcode Ready Ready Preview, Comment Jun 11, 2026 4:45pm

@GulSam00

Copy link
Copy Markdown
Owner Author

/review

@GulSam00

Copy link
Copy Markdown
Owner Author

/improve

@qodo-code-review

qodo-code-review Bot commented Jun 11, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Context used

Grey Divider


Action required

1. Type 미검증 사용 🐞 Bug ⛨ Security
Description
GET /api/searchtype 파라미터를 검증하지 않고 getSearchColumns()가 미지원 type을 컬럼명으로 그대로 사용합니다. 임의의 type으로
잘못된 컬럼/정렬이 구성되어 검색이 500으로 실패하거나 의도치 않은 필터링/정렬이 발생할 수 있습니다.
Code

apps/web/src/app/api/search/route.ts[R32-34]

+function getSearchColumns(type: string): string[] {
+  return SEARCH_COLUMNS[type] ?? [type];
+}
Evidence
API에서 type은 URL에서 그대로 읽히고, 미지원 값은 컬럼 배열로 fallback되어 .or()/.order()에 사용됩니다.

apps/web/src/app/api/search/route.ts[32-34]
apps/web/src/app/api/search/route.ts[59-63]
apps/web/src/app/api/search/route.ts[199-206]

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

## Issue description
`type`이 URL 파라미터에서 그대로 들어오며, 미지원 값은 `getSearchColumns()`에서 `[type]`로 fallback되어 PostgREST 필터/정렬 컬럼으로 사용됩니다. 이로 인해 잘못된 컬럼명으로 쿼리가 깨지거나(500), 의도치 않은 컬럼 기준의 필터/정렬이 가능합니다.

## Issue Context
- 클라이언트 타입(`SearchType`)은 제한돼도, API Route는 외부에서 직접 호출될 수 있습니다.
- 현재 `order`도 `type`에 의존하여 임의 컬럼 정렬로 이어질 수 있습니다.

## Fix Focus Areas
- apps/web/src/app/api/search/route.ts[32-34]
- apps/web/src/app/api/search/route.ts[199-206]

### Suggested fix
1) GET handler에서 `type`을 `all|title|artist|number`로 whitelist 검증하고, 미일치 시 400 반환.
2) `getSearchColumns()`의 fallback (`?? [type]`) 제거(또는 unreachable 처리)하여 미지원 type이 컬럼으로 사용되지 않게 함.
3) `order`도 `{ all: 'title', title: 'title', artist: 'artist', number: 'num_tj' }` 같은 고정 매핑으로 계산.

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


2. .or() 값 미이스케이프 ✓ Resolved 🐞 Bug ⛨ Security
Description
검색 토큰/phrase가 PostgREST .or() 필터 문자열에 그대로 삽입되어, 콤마(,) 같은 구분 문자가 포함되면 조건이 분리되거나 문법이 깨집니다. 이는 잘못된
검색 결과 또는 Supabase 오류(500)를 유발하는 필터 구문 인젝션/파손 문제입니다.
Code

apps/web/src/app/api/search/route.ts[R49-51]

+function buildColumnOrGroup(columns: string[], pattern: string): string {
+  return columns.map(column => `${column}.ilike.${pattern}`).join(',');
+}
Evidence
.or() 필터 문자열이 column.ilike.${pattern} 형태로 직접 조합되며, pattern은 URL에서 온 사용자 입력을 그대로 사용합니다. 이 조합은
콤마를 조건 구분자로 사용하므로 값에 콤마가 들어가면 필터가 파손/분리됩니다.

apps/web/src/app/api/search/route.ts[47-51]
apps/web/src/app/api/search/route.ts[66-85]
apps/web/src/app/api/search/route.ts[199-205]

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

## Issue description
`.or()` 인자는 콤마로 조건을 나누는 PostgREST 문법 문자열인데, 현재 구현은 사용자 입력(`q`→`token`/`phrase`)을 그대로 이어붙입니다. 입력에 `,`/`(` / `)` 등이 포함되면 조건이 의도치 않게 분리되거나 파싱 오류가 발생합니다.

## Issue Context
- `buildColumnOrGroup()`가 `join(',')`로 조건을 생성하므로, 값 자체에 콤마가 포함되는 경우 충돌이 확정적입니다.

## Fix Focus Areas
- apps/web/src/app/api/search/route.ts[47-51]
- apps/web/src/app/api/search/route.ts[66-85]
- apps/web/src/app/api/search/route.ts[199-205]

### Suggested fix
1) `.or()`에 넣기 전에 토큰/phrase를 PostgREST용으로 이스케이프하거나 정규화:
  - 최소한 `,` `(` `)` 같은 구문 구분자를 제거/치환(예: 공백으로 치환 후 재-tokenize)하거나,
  - PostgREST 문법이 허용하는 이스케이프 방식(백슬래시 등)을 적용하는 `escapePostgrestOrValue()`를 도입.
2) 동일 이스케이프를 exact/partial 및 exclusion(`not.ilike`) 경로 모두에 적용하여 일관성 유지.
3) (선택) 허용 문자 whitelist를 두고, 위험 문자가 포함되면 400으로 거절하여 API 안정성을 확보.

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


3. 공백 쿼리 필터 깨짐 ✓ Resolved 🐞 Bug ☼ Reliability
Description
q'   ' 같은 공백-only 문자열이면 상위에서 걸러지지 않고 tokenizeQuery() 결과가 빈 배열이 되어 phrase=''가 생성됩니다. 그 결과
title.ilike. 같은 빈 패턴 필터 문자열이 만들어져 쿼리가 실패(500)하거나 비정상 동작할 수 있습니다.
Code

apps/web/src/app/api/search/route.ts[R59-62]

+  const phrase = buildPhrasePattern(tokenizeQuery(searchText));
+  const columns = getSearchColumns(type);
+
+  return baseQuery.or(buildColumnOrGroup(columns, phrase));
Evidence
GET에서는 query를 trim하지 않은 채 null/빈문자만 검사하며, 새 토큰화/phrase 생성 로직은 토큰이 비면 phrase=''로 이어져 .ilike.
형태의 잘못된 필터 문자열이 생성됩니다.

apps/web/src/app/api/search/route.ts[36-45]
apps/web/src/app/api/search/route.ts[54-63]
apps/web/src/app/api/search/route.ts[199-220]

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 handler는 `if (!query)`만 검사하므로 공백-only 문자열이 통과합니다. 이후 `tokenizeQuery(trim)` → `[]`, `buildPhrasePattern([])` → `''`가 되어 `.or()` 필터 문자열에 빈 패턴이 들어가며 PostgREST 문법 오류/오동작을 유발합니다.

## Issue Context
- 클라이언트는 `.trim()`을 하지만, API는 외부 호출도 가능하며 방어 로직이 필요합니다.

## Fix Focus Areas
- apps/web/src/app/api/search/route.ts[36-45]
- apps/web/src/app/api/search/route.ts[54-63]
- apps/web/src/app/api/search/route.ts[199-220]

### Suggested fix
1) `const raw = searchParams.get('q'); const query = raw?.trim() ?? ''`로 정규화 후 `if (!query) return 400`.
2) `tokenizeQuery()` 결과가 비어있으면(=토큰 0개) 필터 빌더를 호출하지 않도록 방어(400 또는 빈 결과 반환).

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


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Jun 11, 2026

Copy link
Copy Markdown

PR Summary by Qodo

검색: 토큰 AND 매칭 도입 및 NULL-safe 부분 검색 버그 수정
✨ Enhancement 🐞 Bug fix 🕐 20-40 Minutes

Grey Divider

Walkthroughs

Description
• 검색어를 공백 토큰으로 분리해 모든 단어 포함(AND) 시에만 부분 검색되도록 개선
• 정확 검색은 토큰을 %로 연결해 띄어쓰기 차이를 무시한 구(phrase) 매칭으로 상향
• nullable 컬럼의 NOT ILIKE가 NULL로 누락되던 부분 검색 버그를 NULL-safe 조건으로 수정
Diagram
graph TD
  A["Search UI (useSearchSong)"] --> B["/api/search (route.ts)"] --> C["executeSearchQueries()"]
  C --> D["applyExactFilter()"] --> F[("Supabase/PostgREST")] --> G[("songs table")]
  C --> E["applyPartialFilter()"] --> F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Postgres Full-Text Search(tsvector)로 전환
  • ➕ 다중 토큰 AND/OR, 순서, 랭킹을 DB에서 자연스럽게 지원
  • ➕ LIKE/ILIKE 대비 성능 및 품질 개선(인덱스 활용 가능)
  • ➕ NULL/공백/형태소 처리 등 검색 규칙을 일관되게 설계 가능
  • ➖ 마이그레이션(컬럼/인덱스/언어 설정)과 운영 비용 증가
  • ➖ 한글/영문 혼합 시 토크나이저/사전 선택이 필요
  • ➖ 현재 PostgREST 필터 기반 로직보다 구현 범위가 큼
2. Supabase RPC(Stored Procedure)로 검색 로직 캡슐화
  • ➕ 복잡한 AND/OR/NULL-safe 로직을 SQL로 명확하게 표현 가능
  • ➕ 클라이언트/서버에서 동일한 단일 엔드포인트 호출로 단순화
  • ➕ 추후 랭킹/가중치/스코어링 추가가 수월
  • ➖ DB 함수 관리/배포 체계가 필요
  • ➖ 애플리케이션 코드에서 쿼리 가시성이 감소
  • ➖ 테스트/로컬 개발에서 DB 의존도가 커짐

Recommendation: 현 단계에서는 PR의 접근(API에서 토큰화 + PostgREST .or 체이닝 AND + NULL-safe 제외)이 변경 범위 대비 효과가 크고, 클라이언트 공백 처리도 API로 일원화되어 합리적이다. 다만 데이터 규모가 커지거나 랭킹/검색 품질 요구가 증가하면 FTS(tsvector) 또는 RPC로 검색 규칙을 DB 레이어로 이동하는 방안을 고려하는 것이 장기적으로 안정적이다.

Grey Divider

File Changes

Enhancement (1)
route.ts 토큰 AND 부분검색 + phrase 기반 정확검색 + NULL-safe 중복 제외 +54/-33

토큰 AND 부분검색 + phrase 기반 정확검색 + NULL-safe 중복 제외

• 검색 타입별 대상 컬럼 매핑과 토큰화/패턴 생성 헬퍼를 추가해 필터 구성을 단순화했다. 정확 검색은 공백 토큰을 %로 연결한 phrase 패턴으로 띄어쓰기 차이를 무시해 매칭한다. 부분 검색은 토큰별 컬럼 OR 그룹을 .or()로 체이닝해 AND 조건을 만들고, nullable 컬럼에서 NOT ILIKE가 NULL로 누락되던 문제를 'not.ilike OR is.null'로 보정해 정확 티어 중복을 안전하게 제외한다.

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


Bug fix (1)
useSearchSong.ts 한글 공백 제거 로직 삭제로 검색어 공백을 API에 그대로 전달 +2/-6

한글 공백 제거 로직 삭제로 검색어 공백을 API에 그대로 전달

• 기존의 '한글 포함 시 공백 제거' 전처리를 제거해 사용자 입력 공백을 보존한다. 공백 처리 및 토큰화 책임을 /api/search로 이동시켜 클라이언트-서버 간 검색 규칙을 일원화한다.

apps/web/src/hooks/useSearchSong.ts


Grey Divider

Qodo Logo

Comment on lines +32 to +34
function getSearchColumns(type: string): string[] {
return SEARCH_COLUMNS[type] ?? [type];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Type 미검증 사용 🐞 Bug ⛨ Security

GET /api/searchtype 파라미터를 검증하지 않고 getSearchColumns()가 미지원 type을 컬럼명으로 그대로 사용합니다. 임의의 type으로
잘못된 컬럼/정렬이 구성되어 검색이 500으로 실패하거나 의도치 않은 필터링/정렬이 발생할 수 있습니다.
Agent Prompt
## Issue description
`type`이 URL 파라미터에서 그대로 들어오며, 미지원 값은 `getSearchColumns()`에서 `[type]`로 fallback되어 PostgREST 필터/정렬 컬럼으로 사용됩니다. 이로 인해 잘못된 컬럼명으로 쿼리가 깨지거나(500), 의도치 않은 컬럼 기준의 필터/정렬이 가능합니다.

## Issue Context
- 클라이언트 타입(`SearchType`)은 제한돼도, API Route는 외부에서 직접 호출될 수 있습니다.
- 현재 `order`도 `type`에 의존하여 임의 컬럼 정렬로 이어질 수 있습니다.

## Fix Focus Areas
- apps/web/src/app/api/search/route.ts[32-34]
- apps/web/src/app/api/search/route.ts[199-206]

### Suggested fix
1) GET handler에서 `type`을 `all|title|artist|number`로 whitelist 검증하고, 미일치 시 400 반환.
2) `getSearchColumns()`의 fallback (`?? [type]`) 제거(또는 unreachable 처리)하여 미지원 type이 컬럼으로 사용되지 않게 함.
3) `order`도 `{ all: 'title', title: 'title', artist: 'artist', number: 'num_tj' }` 같은 고정 매핑으로 계산.

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

Comment thread apps/web/src/app/api/search/route.ts
Comment thread apps/web/src/app/api/search/route.ts
Qodo 리뷰 대응:
- 토큰화 전 PostgREST .or() 예약문자(, ( ) ") 공백 치환 → 콤마/괄호 포함 검색어로 필터가 분리/파손되던 문제 해결
- GET 핸들러에서 q를 trim 후 빈 값은 400 처리, 유효 토큰 0개면 빈 결과 반환 → 공백/예약문자-only 입력으로 빈 패턴 필터가 생성되던 문제 방어

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@GulSam00 GulSam00 merged commit bdfe381 into develop Jun 11, 2026
1 of 2 checks passed
@GulSam00 GulSam00 deleted the feat/254-searchSpaceHandling branch June 11, 2026 16:45
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.

검색 기능 개편 — 중간 띄어쓰기 처리 및 쿼리 검증

1 participant