Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions docs/api-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@

> ⚠️ **이 절의 근거는 원본 `08_API_명세`가 아니라 front#109다.** Collection 표지 생성 절(§"Collection 표지 생성")과 같은 패턴 — 원본 문서에 이 엔드포인트가 아직 없고, 스키마가 바뀌면 이 문서가 아니라 그 이슈가 기준이다. 이 엔드포인트는 Core API 소속이라(`/image/api/*`와 달리) `httpClient`를 그대로 쓴다.

<!-- 근거: front#109, S15P11A705-344 -->
<!-- 근거: front#109, Jira 작업 -->

- **목적**: 대화 캡처 이미지 1장에서 장소명 후보를 AI가 추출하고, 후보마다 카카오 로컬 검색 결과를 붙여 함께 내려준다. 위 "세 가지 장소 검색" 표에는 없는 **네 번째 경로**다 — 카카오도 `GET /records/map`도 아니고, 이미지 한 장에서 장소를 **발견**하는 용도다. 사용자가 최종 확인·수정한 뒤 기존 `POST /records`로 저장한다. **AI 결과만으로 자동 저장되지 않는다.**
- **요청**: `multipart/form-data`, 필드명 `image`, 이미지 **1장만**, **JPEG/PNG만**, **최대 5 MiB**(front#109). 기존 로그인 쿠키+CSRF 흐름을 그대로 쓴다 — 별도 인증 방식이 없다.
Expand Down Expand Up @@ -135,8 +135,8 @@
- 추가 필드: **`latestCollectionId`**(`number | null`) — 그 Record가 **가장 최근에 담긴** Collection의 id. "가장 최근"의 기준은 컬렉션 내부 정렬과 같은 **담은 시각**이며, 컬렉션에서 **뺀(삭제된) 연결은 판단에서 제외**된다. 어느 Collection에도 담기지 않은 Record는 `null`이다. <!-- 근거: 08_API_명세.md §4.2 -->
- ⚠️ 필드명은 `collectionId`가 **아니다.** 2026-08-04 doc-sync 이전 이 문서와 프론트 Zod 스키마가 `collectionId`로 적혀 있었고, 그대로면 파싱이 조용히 `undefined`가 되어 모든 마커가 "미분류" 색으로 떨어진다.
- 한 Record가 여러 Collection에 담길 수 있으나 이 필드는 **단일 값**이다. 마커 색은 "그 Record가 속한 모든 Collection"이 아니라 "가장 최근 것 하나"를 나타낸다.
- 프론트 처리: `latestCollectionId` 기반 해시로 마커 asset(색) 결정, `null`이면 별도 고정 asset(미분류) 적용. 구현은 `getRecordMarkerAsset`(S15P11A705-307).
- **back 구현은 아직 머지되지 않았다** — back PR #191(`S15P11A705-308`)이 2026-08-04 기준 **OPEN**이다. 그래서 프론트 Zod 스키마는 이 필드를 **optional로 받는다**(필드가 없거나 명시적 `null`이면 동일하게 "미분류"로 취급). 현재 지도 마커가 전부 미분류 색으로 보이는 것은 **정상**이며, #191이 머지·배포된 뒤에야 색이 갈린다. 그 시점에 optional 제거를 검토한다.
- 프론트 처리: `latestCollectionId` 기반 해시로 마커 asset(색) 결정, `null`이면 별도 고정 asset(미분류) 적용. 구현은 `getRecordMarkerAsset`(Jira 작업).
- **back 구현은 아직 머지되지 않았다** — back PR #191(`Jira 작업`)이 2026-08-04 기준 **OPEN**이다. 그래서 프론트 Zod 스키마는 이 필드를 **optional로 받는다**(필드가 없거나 명시적 `null`이면 동일하게 "미분류"로 취급). 현재 지도 마커가 전부 미분류 색으로 보이는 것은 **정상**이며, #191이 머지·배포된 뒤에야 색이 갈린다. 그 시점에 optional 제거를 검토한다.

### [확정] 지도 키워드 칩 (`GET /records/map/keywords`)

Expand All @@ -156,7 +156,7 @@

> **`DELETE /me`는 더 이상 "탈퇴 완료"가 아니다.** `204`(완료) → **`200` + 이동할 곳**으로 바뀌었다. 엔드포인트·메서드는 그대로다. <!-- 근거: 08_API_명세.md §3.6, 06_데이터모델_및_무결성.md §6.9 -->
>
> **프론트 대응 완료** — front #97, back #181(`S15P11A705-285`, 머지됨). `deleteAccount` 응답 파싱 · `WithdrawConfirmDialog` 페이지 이동 · `handleOAuthCallback`의 `WITHDRAWAL_*` 분기까지 반영돼 있다.
> **프론트 대응 완료** — front #97, back #181(`Jira 작업`, 머지됨). `deleteAccount` 응답 파싱 · `WithdrawConfirmDialog` 페이지 이동 · `handleOAuthCallback`의 `WITHDRAWAL_*` 분기까지 반영돼 있다.

탈퇴는 두 단계이며, **공급자 연결 해제가 선행되고 그것이 성공한 경우에만** 데이터가 삭제된다.

Expand Down Expand Up @@ -224,7 +224,7 @@ type PlaceSummary = {
};
```

- back 구현 **머지 완료**(back #184 `S15P11A705-305`, 2026-08-04). `PlaceSummaryResponse`를 쓰는 **모든 응답**에 실린다 — Record 상세뿐 아니라 Collection 상세도 포함이다.
- back 구현 **머지 완료**(back #184 `Jira 작업`, 2026-08-04). `PlaceSummaryResponse`를 쓰는 **모든 응답**에 실린다 — Record 상세뿐 아니라 Collection 상세도 포함이다.
- **`thumbnailUrl`은 `null`일 수 있으나 필드가 생략되지는 않는다.** `null`이거나 이미지 로드에 실패하면 기본 이미지로 폴백한다. <!-- 근거: 08_API_명세.md §11.1 -->
- **4:3 비율**로 제공된다. `aspect-ratio: 4 / 3` + `object-fit: cover`로 표시하면 로딩 전 영역이 확보되고 폭에 적응한다.
- 현 단계 값은 같은 오리진의 절대 경로(`/api/core/images/places/…`)다. 이후 카카오 이미지 검색 API 전환 시 같은 필드에 외부 절대 URL이 들어가며 **프론트 계약은 바뀌지 않는다**. **`VITE_API_BASE_URL`(`/api/core/v1`)을 앞에 붙이지 않는다** — 이 경로는 `v1` 밖이다.
Expand Down Expand Up @@ -371,8 +371,8 @@ type PlaceSummary = {
1. **provider 대소문자** — 경로는 소문자(`kakao`), 응답은 대문자(`KAKAO`). 타입은 대문자로 두고 경로 조립 시 `toLowerCase()`로 매핑한다.
2. **표지 생성 요청의 인증·rate limit** — `POST /image/api/covers`는 GPU 비용이 발생하는데 front#99 예제에 인증 헤더가 없다. 비로그인 허용 여부와 남용 방지 정책이 미확정이다(`05-1_파트간_요구사항.md` §3.2). <!-- 2026-08-05 doc-sync -->
- **구현은 막지 않는다** — 가이드대로 인증 없이 호출한다. 다만 인증이 붙으면 요청 헤더가 바뀌므로, 이미지 서비스 호출부를 전용 클라이언트 한 곳에 모아 그 변경이 한 파일에서 끝나게 한다.
3. **이미지 기반 장소 제안(`POST /places/suggestions`)의 rate limit 구체값** — Gemini Vision 등 분석 비용이 발생하는 경로다. front#109가 `503 PLACE_SUGGESTION_BUSY`(동시 분석 제한)의 **존재**는 명시하지만 임계값(동시 요청 수·시간당 횟수 등)은 없다. 표지 생성(위 2번)과 같은 이유로 협의 필요. <!-- 근거: front#109, S15P11A705-344 -->
4. **`kakaoSearch.status`(`NO_RESULTS`/`FAILED`)가 오류인지 정상 응답 안의 상태 표시인지** — 200 응답 안에 후보별로 내려오는 필드라는 스키마 형태로 미루어 보면 "정상 응답, 개별 후보 실패"에 가깝지만, front#109 본문에는 이 필드명 자체가 없다(이슈는 `warnings[].code`로만 부분 실패를 표현한다). 확정 전까지 프론트는 후보별 안내로만 처리하고 전체 요청 실패로 승격하지 않는다(`PlaceRecordSheet.tsx`, S15P11A705-343). <!-- 근거: front#109 부재, S15P11A705-343/344 -->
3. **이미지 기반 장소 제안(`POST /places/suggestions`)의 rate limit 구체값** — Gemini Vision 등 분석 비용이 발생하는 경로다. front#109가 `503 PLACE_SUGGESTION_BUSY`(동시 분석 제한)의 **존재**는 명시하지만 임계값(동시 요청 수·시간당 횟수 등)은 없다. 표지 생성(위 2번)과 같은 이유로 협의 필요. <!-- 근거: front#109, Jira 작업 -->
4. **`kakaoSearch.status`(`NO_RESULTS`/`FAILED`)가 오류인지 정상 응답 안의 상태 표시인지** — 200 응답 안에 후보별로 내려오는 필드라는 스키마 형태로 미루어 보면 "정상 응답, 개별 후보 실패"에 가깝지만, front#109 본문에는 이 필드명 자체가 없다(이슈는 `warnings[].code`로만 부분 실패를 표현한다). 확정 전까지 프론트는 후보별 안내로만 처리하고 전체 요청 실패로 승격하지 않는다(`PlaceRecordSheet.tsx`, Jira 작업). <!-- 근거: front#109 부재, Jira 작업/344 -->

### 이번에 [협의 필요]에서 제거(확정으로 흡수)됨

Expand Down
18 changes: 9 additions & 9 deletions docs/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
- **컴포넌트에서 API를 직접 호출하지 않는다.** 데이터 흐름은 `docs/architecture.md`를 따른다(화면 → Hook → API 함수 → HTTP Client).
- 서버 상태(TanStack Query)와 UI 상태(Context API)를 한 곳에 섞지 않는다.
- 검증 스키마는 Zod로 정의하고 API 경계에서 파싱한다.
- **하나의 사실을 두 변수가 나눠 들지 않는다.** 특히 외부 라이브러리 인스턴스의 준비 여부가 렌더에 영향을 준다면 ref가 아니라 state로 든다 — ref는 갱신해도 리렌더가 없어 effect가 다시 돌지 않는다(`docs/troubleshooting/2026-08-06-map-remount-ref-state-race.md`, S15P11A705-346).
- **하나의 사실을 두 변수가 나눠 들지 않는다.** 특히 외부 라이브러리 인스턴스의 준비 여부가 렌더에 영향을 준다면 ref가 아니라 state로 든다 — ref는 갱신해도 리렌더가 없어 effect가 다시 돌지 않는다(`docs/troubleshooting/2026-08-06-map-remount-ref-state-race.md`, Jira 작업).
- **Tailwind 클래스는 리터럴로만 쓴다.** Tailwind는 소스를 원시 텍스트로 스캔해 양방향으로 조용히 틀린다 — 문자열 조작으로 만든 클래스는 스캔되지 않아 **스타일이 없고**, 주석에 클래스처럼 생긴 문자열을 적으면 스캔돼 **없는 규칙이 생긴다**. 조건부는 완성된 리터럴 중 하나를 고르는 형태로 쓰고, 주석에서 클래스를 언급할 때 대괄호 arbitrary value를 그대로 적지 않는다(`docs/troubleshooting/2026-08-06-collection-spread-redesign-lessons.md`·`2026-08-06-silent-css-traps-nav-shell.md`).
- **새 컴포넌트를 만들기 전에 기존 구현을 먼저 찾는다.** 병렬 작업에서 배정받은 "내 파일 목록"은 쓰기 권한의 경계이지 **탐색 범위의 경계가 아니다**. 배정 밖 파일을 고쳐야 하면 우회해서 새로 만들지 말고 보고한다(S15P11A705-332).
- **모듈 스코프 플래그·전역 상태의 소비자를 지울 때 생산자도 함께 확인한다.** 타입으로 이어지지 않아 컴파일러가 알려주지 않는다 — 읽는 쪽을 지우면 쓰는 쪽이 아무도 안 보는 값을 계속 쓴다. 당장 지울 수 없으면 후속 정리 대상으로 명시한다(S15P11A705-332 → 350).
- **새 컴포넌트를 만들기 전에 기존 구현을 먼저 찾는다.** 병렬 작업에서 배정받은 "내 파일 목록"은 쓰기 권한의 경계이지 **탐색 범위의 경계가 아니다**. 배정 밖 파일을 고쳐야 하면 우회해서 새로 만들지 말고 보고한다(Jira 작업).
- **모듈 스코프 플래그·전역 상태의 소비자를 지울 때 생산자도 함께 확인한다.** 타입으로 이어지지 않아 컴파일러가 알려주지 않는다 — 읽는 쪽을 지우면 쓰는 쪽이 아무도 안 보는 값을 계속 쓴다. 당장 지울 수 없으면 후속 정리 대상으로 명시한다(Jira 작업 → 350).
- **`httpClient`로 `FormData`(multipart) 요청을 보낼 때는 `Content-Type` 헤더를 지운다.** `httpClient`가 기본 헤더로 `Content-Type: application/json`을 고정하고 있어서, 그대로 두면 브라우저가 multipart boundary를 붙이지 못해 요청이 깨진다(서버가 `image/jpeg` 등을 JSON으로 파싱하려다 실패).

```typescript
Expand All @@ -32,18 +32,18 @@
});
```

`'multipart/form-data'`를 직접 지정하지 않는다 — boundary 값을 브라우저가 요청 생성 시점에 채워야 하므로, 헤더를 아예 비워(`undefined`) 브라우저가 자동으로 채우게 한다. 예시: `suggestPlacesFromImage.ts`(S15P11A705-345).
`'multipart/form-data'`를 직접 지정하지 않는다 — boundary 값을 브라우저가 요청 생성 시점에 채워야 하므로, 헤더를 아예 비워(`undefined`) 브라우저가 자동으로 채우게 한다. 예시: `suggestPlacesFromImage.ts`(Jira 작업).

## 3. Git

- 브랜치: `main`(배포), `dev`(통합). 기능은 항상 `dev`에서 분기한다.
- Jira 이슈키 형식: `S15P11A705-숫자` (예: `S15P11A705-42`).
- Jira 이슈키 형식: `Jira-숫자` (예: `Jira 작업`).

| 대상 | 형식 | 예시 |
| ------- | ---------------------------------- | ------------------------------------- |
| 브랜치 | `feature/S15P11A705-<번호>-<설명>` | `feature/S15P11A705-42-record-map` |
| 커밋 | `type(S15P11A705-<번호>): 설명` | `feat(S15P11A705-42): 지도 마커 조회` |
| PR 제목 | `[S15P11A705-<번호>] 설명` | `[S15P11A705-42] 지도 마커 조회` |
| 브랜치 | `feature/<jira-key>-<설명>` | `feature/<jira-key>-record-map` |
| 커밋 | `type(Jira-<번호>): 설명` | `feat(Jira 작업): 지도 마커 조회` |
| PR 제목 | `[Jira-<번호>] 설명` | `[Jira 작업] 지도 마커 조회` |

- 커밋 `type`: `feat`, `fix`, `refactor`, `docs`, `chore`, `test`, `style` 등 Conventional Commits.
- 설명은 한국어로 간결하게 쓴다.
Expand Down Expand Up @@ -89,4 +89,4 @@

- `/dev/*` 등 개발·QA 전용 라우트는 `router.tsx`에서 `import.meta.env.DEV` 조건으로 라우트 트리 등록 자체를 감싼다(예시: `placeRecordPreviewRoute`). Vite가 빌드 타임에 상수로 치환해 프로덕션 빌드에서는 조건이 `false`로 굳어지므로 라우트가 등록되지 않아 해당 경로는 `NotFoundPage`로 떨어진다 — URL을 알아도 접근할 수 없다. (페이지 컴포넌트 자체는 모듈 그래프에서 정적으로 참조되므로 번들에는 남지만, 라우터가 마운트하지 않아 실행되지는 않는다.)
- `beforeLoad` 인증 가드(`requireLoggedIn` 등)로는 대체하지 않는다. 개발 전용 라우트는 종종 "로그인 없이 테스트 세션을 발급"하는 등 인증 가드와 목적이 상충하는 기능을 담기 때문이다.
- 개발 전용 라우트가 호출하는 API가 있다면, 그 API가 운영 백엔드에도 노출되어 있는지 별도로 확인한다. 프론트에서 라우트를 숨겨도 백엔드 엔드포인트 자체가 운영에 살아 있으면 URL을 아는 누구나 호출할 수 있다 — 이건 프론트만으로는 막을 수 없는 백엔드 이슈다(S15P11A705-342).
- 개발 전용 라우트가 호출하는 API가 있다면, 그 API가 운영 백엔드에도 노출되어 있는지 별도로 확인한다. 프론트에서 라우트를 숨겨도 백엔드 엔드포인트 자체가 운영에 살아 있으면 URL을 아는 누구나 호출할 수 있다 — 이건 프론트만으로는 막을 수 없는 백엔드 이슈다(Jira 작업).
2 changes: 1 addition & 1 deletion docs/reference/05-1_파트간_요구사항.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,6 @@ front#99 가이드의 호출 예제에는 인증 헤더가 없습니다. `POST /
| 1.3 Feed 이벤트 | Front | Feed 동작 |
| 1.4 등급 표시 | Front | 제품 결정 |
| 1.5 입력 크기 상한 | Front | UX — 서버만 막으면 사용자가 다 쓴 뒤 거절당한다 |
| 1.6 표지 생성 플로우 | Front | 구현 정합성 + UX — back은 구현 완료(S15P11A705-322), 프론트 연결만 남음 |
| 1.6 표지 생성 플로우 | Front | 구현 정합성 + UX — back은 구현 완료(Jira 작업), 프론트 연결만 남음 |
| 3.2 생성 요청 인증 | 이미지 서비스(INFRA) | 보안·비용 |
| 3.1 표지 파일 보존 | 이미지 서비스(INFRA) | 확인 완료 — 정리 배치 도입 시 사전 통보 |
2 changes: 1 addition & 1 deletion docs/reference/05_AI_설계.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ openai-text-embedding-3-small-1536-cosine-v1
- Context와 Keyword Preset은 같은 Embedding Profile을 사용합니다.
- **Profile의 정본은 FastAPI 설정의 기본값입니다**(`ai` 레포 `app/core/config.py`). 배포 환경변수 주입은 실험·롤백을 위한 **덮어쓰기**이며 필수가 아닙니다.
- Spring과 FastAPI가 서로 다른 Profile을 갖는 것은 **런타임 대조**로 막습니다. Spring이 검색 요청에 실어 보낸 `embeddingProfile`이 FastAPI 설정과 다르면 요청을 거부하며, 빈 결과를 돌려주지 않습니다 — 빈 결과는 "일치하는 기록이 없음"으로 보여 설정 오류를 숨깁니다. 처리 상세는 `ai` 레포 `docs/spec/model-profile.md` 3.1.
- Spring이 그 값을 어디서 얻는지는 **검색 연동 시점에 정합니다**(`S15P11A705-135`). 소비자가 없는 상태에서 정하면 붙일 때 다시 뒤집힙니다.
- Spring이 그 값을 어디서 얻는지는 **검색 연동 시점에 정합니다**(`Jira 작업`). 소비자가 없는 상태에서 정하면 붙일 때 다시 뒤집힙니다.

Profile 문자열의 버전 표기는 모델·전처리 구성을 식별하는 값이며 Context와 무관합니다.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 백그라운드 폴링이 화면을 옮기면 취소된다 — 구독자 수명과 Provider 위치

- 날짜: 2026-08-06
- 관련 이슈키: S15P11A705-326
- 관련 이슈키: Jira 작업

## 증상

Expand Down Expand Up @@ -66,9 +66,9 @@ Provider가 하는 일은 잡 목록 관리뿐이고, 폴링·저장은 잡마

## 관련 이슈키

- S15P11A705-326 (PR #125) — 이 문서
- S15P11A705-318 — 표지 선택 모달(`CollectionCoverModal`), "나중에 하기"를 두지 않기로 한 결정
- S15P11A705-327 (PR #131) — 장소 추가로 만든 새 컬렉션도 표지 선택을 거치게 함
- Jira 작업 (PR #125) — 이 문서
- Jira 작업 — 표지 선택 모달(`CollectionCoverModal`), "나중에 하기"를 두지 않기로 한 결정
- Jira 작업 (PR #131) — 장소 추가로 만든 새 컬렉션도 표지 선택을 거치게 함

---

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# 컬렉션 펼친 화면 재설계에서 반복된 함정들 (레인 시야·죽은 소비자·Tailwind·지도)

- 날짜: 2026-08-06
- 관련 이슈키: S15P11A705-332
- 관련 이슈키: Jira 작업

여러 세션이 worktree를 나눠(L1 상세 / L2 표지 / L3 책장) 동시에 작업하는 중에 S15P11A705-332를
여러 세션이 worktree를 나눠(L1 상세 / L2 표지 / L3 책장) 동시에 작업하는 중에 Jira 작업를
진행하며 겪은 문제를 모았다. 개별 버그보다 **반복된 패턴**이 남길 가치가 있어 한 문서로 묶는다.

---
Expand Down Expand Up @@ -185,11 +185,11 @@ record 장의 확대 레벨을 6 → 3으로 당기면서 발견했다. 기존

## 관련 이슈키

- S15P11A705-332 (컬렉션 펼친 화면 재설계)
- S15P11A705-207 (오버레이 진입 구분 — `collectionOverlayIntent` 도입)
- S15P11A705-143 (`ShelfExploreSection` 원본)
- S15P11A705-307 (마커 SVG asset·`RecordMapView` 마커 처리)
- S15P11A705-245 / -246 / -171 (목차 fitBounds · 인덱스 레일 · 지도 고정 레벨)
- Jira 작업 (컬렉션 펼친 화면 재설계)
- Jira 작업 (오버레이 진입 구분 — `collectionOverlayIntent` 도입)
- Jira 작업 (`ShelfExploreSection` 원본)
- Jira 작업 (마커 SVG asset·`RecordMapView` 마커 처리)
- Jira 작업 / -246 / -171 (목차 fitBounds · 인덱스 레일 · 지도 고정 레벨)

---

Expand Down
6 changes: 3 additions & 3 deletions docs/troubleshooting/2026-08-06-dev-auth-login-prod-check.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 운영 백엔드 /dev/auth/login 노출 여부 확인

- 날짜: 2026-08-06
- 관련 이슈키: S15P11A705-342
- 관련 이슈키: Jira 작업

## 증상

Expand Down Expand Up @@ -33,9 +33,9 @@ $ curl -s https://pin-log.com/api/core/v1/dev/auth/login

## 재발 방지

- 프론트: 라우트 자체를 `import.meta.env.DEV`로 감싸 프로덕션 번들에서 제외(S15P11A705-342, `docs/conventions.md` 7장에 규칙 추가).
- 프론트: 라우트 자체를 `import.meta.env.DEV`로 감싸 프로덕션 번들에서 제외(Jira 작업, `docs/conventions.md` 7장에 규칙 추가).
- 백엔드: dev 프로파일 전용 컨트롤러가 prod 프로파일 빌드/설정에 포함되지 않는지는 백엔드 팀 확인이 필요하다. 이번 실측상 운영에서 열려 있지 않음이 확인됐으므로 별도 이슈 승격은 하지 않는다.

## 관련 이슈키

S15P11A705-342
Jira 작업
Loading
Loading