Skip to content

[Fix/#222] 일정추천기능수정 - #223

Merged
kimkimgungunwoo merged 1 commit into
developfrom
fix/#222
Jul 31, 2026
Merged

[Fix/#222] 일정추천기능수정#223
kimkimgungunwoo merged 1 commit into
developfrom
fix/#222

Conversation

@kimkimgungunwoo

Copy link
Copy Markdown
Contributor

📌 관련 이슈번호

📌 PR 유형

어떤 변경 사항이 있나요?

  • 새 기능 추가
  • 버그 수정
  • 리팩토링

📌 PR 요약

해당 PR을 간단하게 요약해 주세요

📌 작업 세부 내용

📸 스크린샷 (선택)

🔗 참고 자료

@kimkimgungunwoo kimkimgungunwoo self-assigned this Jul 31, 2026
@kimkimgungunwoo kimkimgungunwoo linked an issue Jul 31, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

추천 배치 유형을 추가하고, 기존 추천과 중복되지 않는 최대 3개의 일정을 재생성하는 서비스를 구현했습니다. 인증된 사용자를 위한 POST schedules/regenerate 엔드포인트와 통계 전용 추천 조회 흐름도 연결했습니다.

Changes

추천 일정 재생성

Layer / File(s) Summary
추천 유형 저장 계약
src/ai/entities/ai-recommend.entity.ts, src/migrations/1785487545211-AddRecommendType.ts
추천 엔티티와 Recommend 테이블에 DIARY, ADDITIONAL, ARCHIVED 유형을 추가했습니다.
추천 생성 및 배치 처리
src/ai/services/ai-gemini.service.ts, src/ai/services/ai-recommend.service.ts
기존 제목과 중복되지 않는 추천을 최대 3개 생성합니다. 기존 ADDITIONAL 배치를 ARCHIVED로 변경하고 새 추천을 ADDITIONAL로 저장합니다. 통계 조회는 ADDITIONAL을 우선 사용하고 없으면 DIARY를 사용합니다.
재생성 API 및 통계 연결
src/ai/ai.controller.ts, src/ai/ai.swagger.ts, src/stats/stats.service.ts
POST schedules/regenerate 엔드포인트를 추가했습니다. 통계 서비스가 전용 추천 조회 메서드를 호출하도록 변경했습니다. Swagger에 성공 및 오류 응답을 정의했습니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • TEAM-DAILOG/BE#144: 초기 추천 생성 흐름을 확장해 재생성 로직을 추가했습니다.
  • TEAM-DAILOG/BE#168: AiControllerRecommendService의 AI 추천 API 흐름과 직접 연결됩니다.
  • TEAM-DAILOG/BE#211: 통계 서비스의 추천 조회 흐름을 확장합니다.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive 이슈 #222에 오류, 원인, 해결 방안, 결과가 없어 구현이 요구사항을 충족하는지 확인할 수 없습니다. 이슈 #222에 오류, 원인, 해결 방안, 결과를 작성하고 각 요구사항과 구현의 대응 관계를 명시하세요.
Out of Scope Changes check ❓ Inconclusive 이슈 #222의 범위가 구체적으로 정의되지 않아 변경된 API, 엔티티, 마이그레이션이 모두 범위 내인지 판단할 수 없습니다. 이슈 #222에 작업 범위를 명시하고 각 변경 파일이 해당 범위에 필요한 이유를 설명하세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 일정 추천 기능 수정이라는 주요 변경 내용을 나타내며 이슈 번호도 포함합니다.
Description check ✅ Passed 설명은 이슈 #222와 일정 추천 기능 리팩토링을 언급하므로 변경 사항과 관련됩니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 fix/#222

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/migrations/1785487545211-AddRecommendType.ts (1)

12-16: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

down()에서 불필요한 COMMENT ON COLUMN 문을 제거하세요.

down()type 컬럼을 곧바로 삭제합니다(Line 14). 삭제 직전에 컬럼 코멘트를 다시 설정하는 문장(Line 13)은 효과가 없습니다. 이 문장을 제거하면 롤백 로직이 더 명확해집니다.

이 파일은 정적 분석 도구가 지적한 Prettier 포맷팅 이슈도 다수 포함합니다. 통합 코멘트에서 함께 다룹니다.

♻️ 제안하는 수정
    public async down(queryRunner: QueryRunner): Promise<void> {
-        await queryRunner.query(`COMMENT ON COLUMN "Recommend"."type" IS '추천 배치 종류 (일기 고정 / 통계 최신 재생성 / 보관된 예전 재생성)'`);
        await queryRunner.query(`ALTER TABLE "Recommend" DROP COLUMN "type"`);
        await queryRunner.query(`DROP TYPE "public"."recommend_type_enum"`);
    }
🤖 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/migrations/1785487545211-AddRecommendType.ts` around lines 12 - 16,
Remove the redundant COMMENT ON COLUMN query from the down() method in the
1785487545211 migration, leaving the column drop and enum type removal
operations unchanged.
🤖 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/ai/services/ai-gemini.service.ts`:
- Around line 53-55: 배포 구성에 GEMINI_REGENERATE_RECOMMEND_PROMPT 환경 변수 항목을 추가하세요.
ai-gemini.service.ts의 regenerateRecommendPrompt 설정 키와 일치하는 예시 값 또는 설명을
.env.example이나 기존 구성 등록 문서에 포함해 누락 시 undefined가 사용되지 않도록 하세요.

In `@src/ai/services/ai-recommend.service.ts`:
- Around line 284-360: Update regenerateRecommendations so archiving the
existing ADDITIONAL records and creating the replacement batch execute
atomically within a database transaction. Acquire a pessimistic lock for the
diary identified by diary.diaryId before these writes to serialize concurrent
regenerations, and ensure saveRecommendation uses the same transaction manager;
preserve the existing validation and no-candidate behavior outside the write
transaction.

---

Nitpick comments:
In `@src/migrations/1785487545211-AddRecommendType.ts`:
- Around line 12-16: Remove the redundant COMMENT ON COLUMN query from the
down() method in the 1785487545211 migration, leaving the column drop and enum
type removal operations unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: abcbfcda-ca2d-452f-b301-0dfb20712324

📥 Commits

Reviewing files that changed from the base of the PR and between 4c09f82 and 9e93d17.

📒 Files selected for processing (7)
  • src/ai/ai.controller.ts
  • src/ai/ai.swagger.ts
  • src/ai/entities/ai-recommend.entity.ts
  • src/ai/services/ai-gemini.service.ts
  • src/ai/services/ai-recommend.service.ts
  • src/migrations/1785487545211-AddRecommendType.ts
  • src/stats/stats.service.ts

Comment on lines +53 to +55
this.regenerateRecommendPrompt = this.configService.get<string>(
'GEMINI_REGENERATE_RECOMMEND_PROMPT',
)!;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify GEMINI_REGENERATE_RECOMMEND_PROMPT is registered in env config files.
set -euo pipefail

fd -HI '\.env' -e example
rg -n 'GEMINI_REGENERATE_RECOMMEND_PROMPT|GEMINI_INITIAL_RECOMMEND_PROMPT' --hidden -g '!node_modules'

Repository: TEAM-DAILOG/BE

Length of output: 152


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo 'Candidate config/env files:'
git ls-files | grep -Ei '(^|/)(\.env.*|.*config.*|^docker-compose.*|.*deployment.*|.*helm.*|.*k8s.*)' || true

echo
echo 'Search for GEMINI_* and similar prompt keys:'
rg -n --hidden -g '!node_modules' 'GEMINI_|REGENERATE_RECOMMEND|RECOMMEND_PROMPT|configService\.get' . || true

echo
echo 'Service file outline and relevant lines:'
sed -n '1,150p' src/ai/services/ai-gemini.service.ts 2>/dev/null || true

Repository: TEAM-DAILOG/BE

Length of output: 7468


GEMINI_REGENERATE_RECOMMEND_PROMPT 설정을 배포 환경에 추가하세요.

src/ai/services/ai-gemini.service.ts가 새 설정 키를 직접 읽고 ! 단언을 사용하므로, 환경 변수 없으면 런타임에 프롬프트에 "undefined"가 포함됩니다. .env.example이나 구성 등록 문서에 GEMINI_REGENERATE_RECOMMEND_PROMPT를 포함하세요.
[ensure]

🤖 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/ai/services/ai-gemini.service.ts` around lines 53 - 55, 배포 구성에
GEMINI_REGENERATE_RECOMMEND_PROMPT 환경 변수 항목을 추가하세요. ai-gemini.service.ts의
regenerateRecommendPrompt 설정 키와 일치하는 예시 값 또는 설명을 .env.example이나 기존 구성 등록 문서에 포함해
누락 시 undefined가 사용되지 않도록 하세요.

Comment on lines +284 to +360
// 통계에서 "다른 일정 추천받기" 호출. 지금까지 나온(타입 무관) 모든 제목을 제외 목록으로
// 넘겨서 최대 3개(부족하면 그보다 적게)를 새로 만든다. 기존 ADDITIONAL 배치는 삭제하지 않고
// ARCHIVED로 상태만 바꿔 보존하며, 새 배치만 ADDITIONAL로 저장해 통계에 우선 노출시킨다.
async regenerateRecommendations(userId: number): Promise<RecommendListDTO> {
const diary = await this.findTodayDiary(userId);

if (!diary) {
throw new ConflictException('오늘 작성된 일기가 없습니다.');
}

const ownedCategories = await this.categoryRepository.find({
where: { userId },
});

if (ownedCategories.length === 0) {
throw new ConflictException(
'일정 추천을 받으려면 먼저 카테고리를 생성해야 합니다.',
'NO_CATEGORY',
);
}

const allExistingRecommends = await this.recommendRepository.find({
where: { diary: { diaryId: diary.diaryId } },
});

const items = await this.geminiService.generateRegeneratedRecommendations(
diary.content,
ownedCategories.map((category) => ({
categoryId: category.categoryId,
categoryName: category.categoryName,
})),
allExistingRecommends.map((r) => r.title),
);

// 유효한(카테고리까지 매칭되는) 후보가 하나도 없으면 기존 ADDITIONAL 배치를 건드리지 않는다 —
// 재생성 실패가 "직전에 보여주던 배치가 사라지는" 부작용을 남기면 안 되기 때문.
const candidates = items
.slice(0, MAX_RECOMMENDATION_BATCH_COUNT)
.filter(
(item): item is RecommendationItem & { scheduleTitle: string } =>
!!item.scheduleTitle &&
ownedCategories.some((category) => category.categoryId === item.categoryId),
);

if (candidates.length === 0) {
throw new ConflictException(
'더 이상 추천할 수 있는 일정이 없습니다.',
'NO_MORE_RECOMMENDATIONS',
);
}

await this.recommendRepository.update(
{
diary: { diaryId: diary.diaryId },
type: RecommendType.ADDITIONAL,
},
{ type: RecommendType.ARCHIVED },
);

const created: RecommendEntity[] = [];

for (const item of candidates) {
// candidates는 이미 카테고리 매칭까지 검증됐으므로 saveRecommendation은 항상 성공한다.
const recommend = await this.saveRecommendation(
diary,
item,
ownedCategories,
RecommendType.ADDITIONAL,
);

if (recommend) {
created.push(recommend);
}
}

return new RecommendListDTO(created);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

아카이브·저장 시퀀스에 트랜잭션과 동시성 보호가 없습니다.

regenerateRecommendations는 기존 ADDITIONAL 배치를 ARCHIVED로 변경하는 작업(Line 335-341)과 새 ADDITIONAL 배치를 저장하는 작업(Line 345-357)을 트랜잭션 없이 순차적으로 실행합니다. 이로 인해 두 가지 문제가 발생합니다.

  1. 동시성 경쟁 조건: 이 API를 짧은 간격으로 두 번 호출하면(예: 사용자의 중복 클릭), 두 요청 모두 같은 기존 상태를 읽고 각각 아카이브·저장을 수행할 수 있습니다. 그 결과 "diary당 최대 1세트만 존재"라는 불변식(Line 227-228, 254 주석)이 깨지고, getTodayRecommendationsForStats가 3개를 초과하는 ADDITIONAL 레코드를 반환할 수 있습니다.
  2. 부분 실패: 아카이브 업데이트가 성공한 뒤 저장 루프 중간에 오류가 발생하면, 기존 배치는 이미 사라졌는데 새 배치는 불완전하게 남을 수 있습니다.

트랜잭션으로 두 단계를 묶고, 동시 호출을 막기 위한 잠금(예: diary_id에 대한 비관적 잠금) 또는 (diary_id) 대상 부분 유니크 제약(type = 'ADDITIONAL'인 행에 대해)을 추가하는 방안을 검토하세요.

// 개념적 예시: DataSource를 주입해 트랜잭션으로 묶는 방법
async regenerateRecommendations(userId: number): Promise<RecommendListDTO> {
  // ...검증 로직...
  return this.dataSource.transaction(async (manager) => {
    // 동시 호출을 막기 위해 diary 행에 비관적 잠금을 건 뒤 아카이브·저장을 수행
    await manager.update(
      RecommendEntity,
      { diary: { diaryId: diary.diaryId }, type: RecommendType.ADDITIONAL },
      { type: RecommendType.ARCHIVED },
    );
    // ...saveRecommendation 호출들을 같은 manager로 수행...
  });
}
🧰 Tools
🪛 ESLint

[error] 325-325: Replace (category)·=>·category.categoryId·===·item.categoryId with ⏎············(category)·=>·category.categoryId·===·item.categoryId,⏎··········

(prettier/prettier)

🤖 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/ai/services/ai-recommend.service.ts` around lines 284 - 360, Update
regenerateRecommendations so archiving the existing ADDITIONAL records and
creating the replacement batch execute atomically within a database transaction.
Acquire a pessimistic lock for the diary identified by diary.diaryId before
these writes to serialize concurrent regenerations, and ensure
saveRecommendation uses the same transaction manager; preserve the existing
validation and no-candidate behavior outside the write transaction.

@kimkimgungunwoo
kimkimgungunwoo merged commit 615e2f4 into develop Jul 31, 2026
2 checks passed
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.

[Fix] 일정추천 API 수정

1 participant