# 2026-06-29 게임 "좋아요" 서버 영속화

세션: `20260629-151216` · 검증: L1 PASS (L2 dev DB skip)

## 한 줄 요약

게임 좋아요가 서버에 전혀 저장되지 않고 브라우저 `localStorage` 로만 토글되던 버그를 수정. 신규 `POST /game/{id}/like` 토글 엔드포인트를 추가해 `game_likes` row 와 `games.like_count` 비정규화 컬럼을 단일 트랜잭션으로 동기 변경하고, explore 목록은 무변경으로 증가된 컬럼을 읽어 좋아요 수가 반영된다.

## 배경 / 버그

- 증상: 게임 상세페이지에서 좋아요를 눌러도 explore(탐색) 페이지에서 좋아요 수가 올라가지 않는다.
- 근본원인(seed 가정 반전): "explore 와 상세가 서로 다른 카운트 소스를 읽는 동기화 불일치" 가 아니라 **좋아요가 어떤 영속 저장소에도 쓰이지 않았다.**
  - 좋아요 토글 서버 엔드포인트가 애초에 부재 (GameController 매핑 0개).
  - `GameLikesMapper.addGameLike/updateGameLike` 는 호출자 0건 dead code.
  - 상세 JSP 좋아요 버튼이 서버 호출 없이 `localStorage('bibimbap-game-liked')` 만 토글하고 화면 카운트를 `baseLikes ± 1` 로 로컬 계산.
  - explore 5개 조회 + 상세 모두 동일하게 `games.like_count` 컬럼을 직접 SELECT — 그 컬럼이 갱신되지 않으므로 영원히 그대로.
- 근본원인 상세: `.atp/work-session/20260629-151216/research/like-count-rootcause.md`

## 사용자 결정

- D1 (수정 범위 / 카운트 모델): 서버 영속 + `games.like_count` 비정규화 컬럼 동기화. explore 쿼리는 무변경.
- D2 (좋아요 주체): 로그인 사용자 기준(`sessionUserId`). 미로그인 시 401 로 거부하고 로그인 유도. 사용자당 게임당 1회(멱등 토글).

설계 상세: `.atp/work-session/20260629-151216/artifacts/like-persistence-design.md`

## 변경 내용

### 신규 엔드포인트: `POST /game/{id}/like`

- `GameController.toggleLike(id, request, session)` (`@Transactional`).
- 게이트 순서: CSRF(403) → 로그인(401) → 게임 존재(404). 기존 컨트롤러 메서드와 동일.
- 동작: 현재 좋아요 row 조회 후 분기 — 없으면 추가(`addGameLike` + `incrementLikeCount`, liked=true), 있으면 취소(`deleteByGameAndUser` + `decrementLikeCount`, liked=false). 변경 직후 컬럼을 재조회해 진실값 응답.
- 응답(200): `{ status:200, liked:<bool>, likeCount:<int> }`. 실패: 403 / 401 / 404.
- 계약 상세는 contracts 가 아닌 본 문서 + 설계 문서에 기록(별도 contract 기준 문서는 미생성 — 아래 concerns 참조).

### 매퍼

- `GameLikesMapper`: `findByGameAndUser(gameId, userKey)` / `deleteByGameAndUser(gameId, userKey)` 신규. `addGameLike` 는 기존 재사용. `user_key = String.valueOf(sessionUserId)`.
- `GamesMapper`: `incrementLikeCount(id)`(+1) / `decrementLikeCount(id)`(`GREATEST(like_count - 1, 0)` 음수 방어) / `getLikeCount(id)` 신규. 모두 `#{}` 바인딩, `${}` 동적치환 없음.

### 상세 컨트롤러 / JSP

- `addGameModel` 이 현재 사용자의 기존 좋아요 여부를 `liked` 모델 속성으로 주입. 카탈로그 폴백 경로는 `liked=false`.
- `game-detail.jsp` 좋아요 버튼을 `localStorage` 토글 → `fetch POST`(`window.BibimbapCsrf.headers`)로 교체. 서버 응답 `likeCount`/`liked` 로 `textContent` 갱신. 초기 상태는 서버 주입 `${liked}` 사용.

### explore 무변경

- explore 조회 쿼리(`GamesMapper` 의 `g.like_count AS likeCount` SELECT)는 손대지 않음. 컬럼이 갱신되므로 자동 반영. (검증 AC-8: `g.like_count AS likeCount` 패턴 7건 유지)

### DB 스키마

- `db/schema.sql`: `game_likes` 에 `UNIQUE(game_id, user_key)` 제약 반영(비권위 복원본 동기).
- `db/migrations/20260629-game-likes-unique.sql`: 신규. **운영 DB 미적용** — 중복 row 점검 SELECT 선행 후 적용 대기. 적용 절차는 [maintenance/post-deploy-verification-checklist.md](../maintenance/post-deploy-verification-checklist.md) 참조.

## 검증

- L1 PASS: `test-compile` + `GameLikeControllerTest` 7/7 + `*ControllerTest` 회귀 219/219 GREEN (eclipse-temurin:21-jdk 컨테이너).
- L2 (dev DB contract): skip — dev DB 미기동 + harness 미구축. like 매퍼는 INSERT/DELETE/UPDATE int 반환 위주라 camelCase Map alias 케이스폴딩 리스크 낮음.
- 검증 상세: `.atp/work-session/20260629-151216/artifacts/verification-result.md`

## 잔여 / 미수행 (needs_user_verification)

- UNIQUE 마이그레이션(`db/migrations/20260629-game-likes-unique.sql`) 운영 적용 미수행 — 중복 점검 후 적용 게이트 대기.
- 실환경 스모크 미수행: 로그인 → 상세 좋아요 클릭 → explore 카운트 +1 + 상세 초기 `aria-pressed` 상태(AC-7) 수동 확인.
- L2 dev DB contract: PostgreSQL dev 기동 후 mapper SQL 실DB 동작 확인.
- 기존 `like_count` 컬럼값과 `game_likes` row 수 초기 불일치는 보존(±1 상대 증감, 절대 재계산은 비목표). 선택적 정합 보정 쿼리는 설계 문서 롤아웃 §3 참조.

## 관련 문서

- 보안 체크리스트 B3(좋아요 서버 영속화 연결): [security/security-remediation-checklist.md](../security/security-remediation-checklist.md)
- 운영 적용 절차: [maintenance/post-deploy-verification-checklist.md](../maintenance/post-deploy-verification-checklist.md)
- 프로젝트 분석(미완성 기능 항목): [analysis/2026-06-16-project-analysis.md](../analysis/2026-06-16-project-analysis.md)
- 댓글 서버 영속화(선행 유사 작업): [changes/2026-06-18-w3-2-comments-reviews.md](./2026-06-18-w3-2-comments-reviews.md)
