Skip to content

feat(auth): 카카오 로그인 시 약관 동의 레코드 저장 구현 - #72

Merged
evenif99 merged 4 commits into
devfrom
feature/terms-agreement-records
Jun 19, 2026
Merged

evenif99 merged 4 commits into
devfrom
feature/terms-agreement-records

Conversation

@evenif99

@evenif99 evenif99 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • 카카오 로그인 시 user_terms_agreements 테이블에 약관 동의 레코드(service, privacy) 자동 생성
  • saveFirstLoginFields에서 $transaction으로 유저 업데이트와 약관 동의 레코드 생성을 원자적 처리
  • 약관 버전 상수 정의 (날짜 기반 관리: 2026-06-18)
  • 기존 유저 대상 backfill 스크립트 추가 (scripts/backfill-user-terms-agreements.ts)
  • src/types/user.ts 공유 타입 파일 추출 (UserMeDto, PatchUserBody, VALID_MBTI)
  • src/app/api/users/me/route.ts에서 인라인 타입/상수 제거, 공유 타입 import로 전환

변경 파일

  • src/domains/auth/constants.ts — 약관 버전 상수 (SERVICE, PRIVACY / 2026-06-18)
  • src/lib/auth/index.ts — 로그인 시 약관 동의 레코드 생성 로직 추가 (트랜잭션)
  • scripts/backfill-user-terms-agreements.ts — 기존 유저 backfill 스크립트
  • src/types/user.ts — 공유 유저 타입 (UserMeDto, PatchUserBody, VALID_MBTI)
  • src/app/api/users/me/route.ts — 인라인 타입 제거, 공유 타입 import 적용

Test plan

  • 신규 카카오 로그인 후 user_terms_agreements 테이블에 2건(service, privacy) 생성 확인
  • 기존 유저는 user_terms_agreements 레코드 없음 확인
  • npx tsx scripts/backfill-user-terms-agreements.ts 실행 후 기존 유저 backfill 정상 동작 확인
  • backfill 후 유저 수 × 2 = 레코드 수 정합성 확인
  • src/types/user.ts 타입이 API route에서 정상 import 확인

🤖 Generated with Claude Code

Summary by CodeRabbit

릴리스 노트

  • New Features
    • 기존 사용자의 약관 동의 이력을 자동으로 기록하도록 개선
    • 사용자 프로필 정보(이름, 닉네임, MBTI) 관리를 위한 타입 정의 추가

- 로그인 시 user_terms_agreements 테이블에 service, privacy 약관 동의 레코드 자동 생성
- saveFirstLoginFields에서 $transaction으로 유저 업데이트와 약관 동의 레코드 생성을 원자적 처리
- 약관 버전 상수 정의 (날짜 기반: 2026-06-18)
- 기존 유저 대상 backfill 스크립트 추가 (scripts/backfill-user-terms-agreements.ts)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
talky-owl Ready Ready Preview, Comment Jun 19, 2026 7:38am
talky-owl-xqdp Ready Ready Preview, Comment Jun 19, 2026 7:38am

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: cd2cf1df-7961-4efb-b558-514908c2b829

📥 Commits

Reviewing files that changed from the base of the PR and between a7bb550 and 3906a8b.

📒 Files selected for processing (1)
  • src/lib/auth/index.ts

📝 Walkthrough

Walkthrough

saveFirstLoginFields의 기존 사용자 분기에 CURRENT_TERMS_VERSIONS를 기반으로 userTermsAgreement.createManyprisma.$transaction으로 묶어 원자적 처리하는 로직을 추가한다. 아울러 사용자 프로필 관련 공개 타입(UserMeDto, PatchUserBody, VALID_MBTI)을 신규 정의한다.

Changes

약관 동의 트랜잭션 확장 및 사용자 프로필 타입 정의

Layer / File(s) Summary
사용자 프로필 타입 및 MBTI 상수 정의
src/types/user.ts
UserMeDto(id 필수, name/nickname/mbti nullable), PatchUserBody(name/nickname 선택, mbti nullable 선택) 인터페이스와 16개 리터럴을 as const로 고정한 VALID_MBTI 상수를 신규 추가한다.
기존 사용자 분기 약관 동의 트랜잭션 처리
src/lib/auth/index.ts
saveFirstLoginFields의 기존 사용자 분기에 prisma.$transaction을 도입하여 user.updateuserTermsAgreement.createMany(CURRENT_TERMS_VERSIONS 전 항목, skipDuplicates: true)를 원자적으로 처리하고, createUser 이벤트 주변 설명 주석을 삭제한다.

예상 코드 리뷰 노력

🎯 2 (Simple) | ⏱️ ~10 minutes

관련 가능성 있는 PR

  • I5-Project/TALKY-OWL#50: src/lib/auth/index.ts에서 카카오 최초 로그인 시 kakaoId/termsAgreedAt 저장 흐름을 공유하며, 이번 PR의 약관 동의 쓰기 변경과 직접 연관된다.
  • I5-Project/TALKY-OWL#59: saveFirstLoginFields 함수를 signIn/events.createUser에서 호출하도록 수정한 PR으로, 이번 PR이 변경하는 동일 함수와 파일에 직접 연결된다.

Poem

🐰 토끼가 약관 도장 꾹 찍었네,
트랜잭션 안에서 한 번에 쏙!
중복은 skipDuplicates로 쫓아내고,
VALID_MBTI 열여섯 개도 딱 정해뒀지.
이제 프로필도 빈틈없이 튼튼해~ 🌿

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning PR 설명이 제공되었으나, 저장소의 필수 템플릿 섹션(작업 내용, 담당 영역, 관련 Issue, 요구사항 등)이 대부분 채워지지 않았습니다. 요약과 변경 파일 목록만 제공되었습니다. 저장소 템플릿의 필수 섹션들(1-12번)을 작성해주세요. 특히 담당 영역, 관련 Issue, 관련 요구사항(FR/NFR), 테스트 결과, 작업 범위 확인, 보안/개인정보 확인, DB/API 변경 여부 등을 명시해주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 PR의 핵심 변경사항을 명확하게 설명하고 있습니다. 카카오 로그인 시 약관 동의 레코드 저장 구현이라는 주요 기능이 잘 드러나 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/terms-agreement-records

Comment @coderabbitai help to get the list of available commands and usage tips.

- API 라우트 디렉토리 이동 (src/app/api/user/me → src/app/api/users/me)
- domains/user/hooks.ts 내 API 호출 경로 일괄 수정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/backfill-user-terms-agreements.ts`:
- Around line 13-45: The backfill script is hardcoding the INITIAL_TERMS_VERSION
constant and TermsType values in the main() function instead of importing them
from the centralized src/domains/auth/constants.ts file. Remove the hardcoded
INITIAL_TERMS_VERSION constant declaration at the top of the file and add
imports for both INITIAL_TERMS_VERSION and TermsType from
src/domains/auth/constants.ts to ensure consistency between the backfill script
and the authentication login path version management.
- Around line 16-20: In the usersWithoutAgreements query filter, change the
condition from `termsAgreements: { none: {} }` to `termsAgreements: { some: {}
}` to include users with existing (even partially corrupted) agreement records
in the backfill scope. Additionally, add a unique constraint to the
UserTermsAgreement schema model definition such as `@@unique([userId,
termsType])` to establish the unique constraint that the `skipDuplicates: true`
parameter in the createMany operation depends on to prevent duplicate
insertions.

In `@src/lib/auth/index.ts`:
- Around line 46-54: The userTermsAgreement.createMany operation in the
saveFirstLoginFields function lacks idempotency protection, allowing duplicate
records when the function is called multiple times (from both signIn and
createUser code paths). Fix this by adding a composite unique constraint on the
UserTermsAgreement model for the fields userId, termsType, and termsVersion in
the Prisma schema, and then add the skipDuplicates: true option to all
createMany calls for userTermsAgreement (there are multiple calls across the
saveFirstLoginFields function at different sections) to prevent duplicate
inserts while maintaining transaction atomicity.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b8b412d-4cdf-4ce0-b42f-9bb384c862de

📥 Commits

Reviewing files that changed from the base of the PR and between b8d7623 and 7755a9e.

📒 Files selected for processing (3)
  • scripts/backfill-user-terms-agreements.ts
  • src/domains/auth/constants.ts
  • src/lib/auth/index.ts

Comment on lines +13 to +45
const INITIAL_TERMS_VERSION = '2026-06-18'

async function main() {
const usersWithoutAgreements = await prisma.user.findMany({
where: {
termsAgreedAt: { not: null },
termsAgreements: { none: {} },
},
select: { id: true, termsAgreedAt: true },
})

console.log(`backfill 대상 유저 수: ${usersWithoutAgreements.length}`)

if (usersWithoutAgreements.length === 0) {
console.log('backfill 대상 없음. 종료.')
return
}

const records = usersWithoutAgreements.flatMap((user) => [
{
userId: user.id,
termsType: TermsType.SERVICE,
termsVersion: INITIAL_TERMS_VERSION,
isRequired: true,
agreedAt: user.termsAgreedAt!,
},
{
userId: user.id,
termsType: TermsType.PRIVACY,
termsVersion: INITIAL_TERMS_VERSION,
isRequired: true,
agreedAt: user.termsAgreedAt!,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

백필 스크립트가 중앙 약관 버전 상수를 우회하고 있습니다.

src/domains/auth/constants.ts에서 버전을 중앙관리하도록 바뀌었는데, 여기서는 INITIAL_TERMS_VERSION/TermsType를 하드코딩하고 있어 로그인 경로와 백필 경로의 버전 계약이 쉽게 분기됩니다.

제안 변경안
-import { PrismaClient, TermsType } from '`@prisma/client`'
+import { PrismaClient } from '`@prisma/client`'
+import { CURRENT_TERMS_VERSIONS } from '../src/domains/auth/constants'

 const prisma = new PrismaClient()

-const INITIAL_TERMS_VERSION = '2026-06-18'
-
 async function main() {
@@
-  const records = usersWithoutAgreements.flatMap((user) => [
-    {
-      userId: user.id,
-      termsType: TermsType.SERVICE,
-      termsVersion: INITIAL_TERMS_VERSION,
-      isRequired: true,
-      agreedAt: user.termsAgreedAt!,
-    },
-    {
-      userId: user.id,
-      termsType: TermsType.PRIVACY,
-      termsVersion: INITIAL_TERMS_VERSION,
-      isRequired: true,
-      agreedAt: user.termsAgreedAt!,
-    },
-  ])
+  const records = usersWithoutAgreements.flatMap((user) =>
+    Object.values(CURRENT_TERMS_VERSIONS).map((terms) => ({
+      userId: user.id,
+      termsType: terms.type,
+      termsVersion: terms.version,
+      isRequired: terms.isRequired,
+      agreedAt: user.termsAgreedAt!,
+    })),
+  )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/backfill-user-terms-agreements.ts` around lines 13 - 45, The backfill
script is hardcoding the INITIAL_TERMS_VERSION constant and TermsType values in
the main() function instead of importing them from the centralized
src/domains/auth/constants.ts file. Remove the hardcoded INITIAL_TERMS_VERSION
constant declaration at the top of the file and add imports for both
INITIAL_TERMS_VERSION and TermsType from src/domains/auth/constants.ts to ensure
consistency between the backfill script and the authentication login path
version management.

Comment on lines +16 to +20
const usersWithoutAgreements = await prisma.user.findMany({
where: {
termsAgreedAt: { not: null },
termsAgreements: { none: {} },
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) 백필 필터가 '레코드 0개'만 대상으로 하는지 확인"
rg -n -C3 'termsAgreements:\s*\{\s*none:\s*\{\s*\}\s*\}' scripts/backfill-user-terms-agreements.ts

echo
echo "2) UserTermsAgreement 모델의 유니크 제약 확인"
rg -n -A30 -B2 'model UserTermsAgreement' prisma/schema.prisma
rg -n '@@unique|`@unique`' prisma/schema.prisma

Repository: I5-Project/TALKY-OWL

Length of output: 3124


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Lines 16-50+ of backfill script to see full context ==="
cat -n scripts/backfill-user-terms-agreements.ts | head -60

echo
echo "=== Check for skipDuplicates and createMany usage ==="
rg -n -A5 -B5 'skipDuplicates|createMany|createManyAndReturn' scripts/backfill-user-terms-agreements.ts

Repository: I5-Project/TALKY-OWL

Length of output: 2345


쿼리 필터가 부분 손상 유저를 영구 제외하고, skipDuplicates가 실제로는 작동하지 않습니다.

termsAgreements: { none: {} } (line 19)는 약관 레코드가 0개인 유저만 선택합니다. 이미 1개 이상의 부분 손상 상태인 유저는 영구적으로 백필 대상에서 제외되어 데이터 불일치가 발생합니다.

또한 createMany(..., skipDuplicates: true) (line 50)는 UserTermsAgreement 모델에 유니크 제약이 없으므로 실제로 중복을 방지하지 못합니다. skipDuplicates는 유니크 제약에 의존하는데, 현재 스키마에는 그런 제약이 없습니다(userId + termsType 복합 유니크 등이 필요).

해결책:

  1. 필터를 termsAgreements: { some: {} } 또는 별도 로직으로 변경하여 부분 손상 유저도 포함
  2. UserTermsAgreement 스키마에 적절한 유니크 제약 추가 (예: @@unique([userId, termsType]))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/backfill-user-terms-agreements.ts` around lines 16 - 20, In the
usersWithoutAgreements query filter, change the condition from `termsAgreements:
{ none: {} }` to `termsAgreements: { some: {} }` to include users with existing
(even partially corrupted) agreement records in the backfill scope.
Additionally, add a unique constraint to the UserTermsAgreement schema model
definition such as `@@unique([userId, termsType])` to establish the unique
constraint that the `skipDuplicates: true` parameter in the createMany operation
depends on to prevent duplicate insertions.

Comment thread src/lib/auth/index.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/api/users/me/route.ts`:
- Around line 15-17: Move the session retrieval call to
getServerSession(authOptions) and the subsequent getSessionUserId(session)
validation inside the try block of both the GET and PATCH functions in the route
handler. Currently these calls are outside the try/catch block, which means any
exceptions thrown by getServerSession will not be caught and will result in a
default 500 response instead of the properly formatted ApiResponse structure.
Ensure that both functions handle session initialization and userId extraction
within their respective try blocks to comply with the API response format
guidelines.
- Around line 139-146: The catch block handling the prisma.user.update call is
missing a check for error code P2025, which is thrown when the user record no
longer exists in the database. Add an additional condition after the existing
P2002 check to detect when err.code equals P2025, and return a 404
NextResponse.json with an ApiResponse containing success: false and error code
USER_NOT_FOUND with an appropriate message, matching the pattern used in the GET
endpoint for consistency.
- Around line 78-107: The code uses TypeScript's `as PatchBody` type assertion
which provides no runtime validation, allowing non-string values to be passed
and causing errors when calling `.trim()` on `body.name` and `body.nickname`, or
`.toUpperCase()` on `body.mbti`. Before calling any string methods on these
fields, add `typeof` checks to verify that `body.name`, `body.nickname`, and
`body.mbti` are actually strings, and handle the cases where they are not (such
as when they are null, undefined, or numbers) by adding appropriate error
entries to the `fieldErrors` array with appropriate error codes and messages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d637146-6249-48dd-a022-f8ce3ee0fb95

📥 Commits

Reviewing files that changed from the base of the PR and between 7755a9e and 3e42ccc.

📒 Files selected for processing (3)
  • src/app/api/user/me/route.ts
  • src/app/api/users/me/route.ts
  • src/domains/user/hooks.ts
💤 Files with no reviewable changes (1)
  • src/app/api/user/me/route.ts

Comment on lines +15 to +17
export async function GET() {
const session = await getServerSession(authOptions)
const userId = getSessionUserId(session)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

세션 조회도 try/catch 안에서 처리하세요.

getServerSession(authOptions)가 예외를 던지면 현재 catch를 거치지 않아 ApiResponse 형식 대신 기본 500 응답이 나갈 수 있습니다. GET/PATCH 모두 세션 조회와 userId 검증을 핸들러 try 블록 안으로 옮겨 주세요.

As per coding guidelines, API responses must follow common response structure format; Try-catch error handling must be implemented in API Route Handlers and Server Services.

Also applies to: 66-68

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/users/me/route.ts` around lines 15 - 17, Move the session
retrieval call to getServerSession(authOptions) and the subsequent
getSessionUserId(session) validation inside the try block of both the GET and
PATCH functions in the route handler. Currently these calls are outside the
try/catch block, which means any exceptions thrown by getServerSession will not
be caught and will result in a default 500 response instead of the properly
formatted ApiResponse structure. Ensure that both functions handle session
initialization and userId extraction within their respective try blocks to
comply with the API response format guidelines.

Source: Coding guidelines

Comment thread src/app/api/users/me/route.ts Outdated
Comment thread src/app/api/users/me/route.ts Outdated
Comment thread src/app/api/users/me/route.ts Outdated
}
}

const VALID_MBTI = [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

user type 파일 하나 만들까요? 저도 같은 거 써서 그냥 같이 쓸까봐요

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

그러는게 좋을 듯 싶슴다

Comment thread src/app/api/users/me/route.ts Outdated
if (body.nickname !== undefined) {
const trimmed = body.nickname.trim()
if (!trimmed || trimmed.length < 2 || trimmed.length > 20) {
fieldErrors.push({ field: 'nickname', code: 'INVALID_NICKNAME', message: '닉네임은 2~20자로 입력해주세요.' })

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

이것도 수정해야겠네요

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

확인했슴다

Comment thread src/app/api/users/me/route.ts Outdated

if (fieldErrors.length > 0) {
return NextResponse.json<ApiResponse>(
{ success: false, error: { code: 'VALIDATION_ERROR', message: '입력값을 확인해주세요.', fieldErrors } },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

음 지시멘트가 너무 애매해서.. 한글 ㅇㅇ자 문장부호 안된다 이런식으로 작성해야될 것 같아요

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

확인했슴다

Move UserMeDto, PatchUserBody, and VALID_MBTI from route handler
into a shared type file for reuse across API and client code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- src/app/api/user/me/route.ts: dev 버전 유지 (프로필 이미지 업로드 포함)
- src/app/api/users/me/route.ts: dev 버전 유지
- src/domains/user/hooks.ts: dev 버전 유지 (api 모듈 import 구조)
- src/lib/auth/index.ts: 양쪽 변경사항 병합 (user.name 체크 + skipDuplicates)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

Deployment failed with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/lsgs-projects-34d31fd6?upgradeToPro=build-rate-limit

@evenif99
evenif99 merged commit aa1f436 into dev Jun 19, 2026
3 checks passed
@evenif99
evenif99 deleted the feature/terms-agreement-records branch June 19, 2026 07:41
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.

2 participants