|
| 1 | +"""리포트 생성 안정 라우터 — ``/api/reports/*``. |
| 2 | +
|
| 3 | +Spring이 호출하는 최종 안정 경로. 현재는 요청 스키마 검증 후 고정 더미 응답 반환. |
| 4 | +cutover 시 각 핸들러 본문을 ``report_service.run_*`` 호출로 교체하면 운영 전환 완료. |
| 5 | +""" |
| 6 | + |
| 7 | +from __future__ import annotations |
| 8 | + |
| 9 | +from fastapi import APIRouter, Depends |
| 10 | + |
| 11 | +from app.core.security import require_internal_token |
| 12 | +from app.schemas.report import ( |
| 13 | + AiCareerBrandingResponse, |
| 14 | + AiCareerHighlightsResponse, |
| 15 | + AiCareerNarrativeResponse, |
| 16 | + AiCareerStrengthsAndInterviewResponse, |
| 17 | + AiMiniReportResponse, |
| 18 | + AiReportRequest, |
| 19 | + InterviewQuestion, |
| 20 | + StrengthItem, |
| 21 | +) |
| 22 | +from app.services.report._compute import ( |
| 23 | + build_evidences, |
| 24 | + competency_frequency, |
| 25 | + top_detail_tag_stats, |
| 26 | + top_detail_tags, |
| 27 | +) |
| 28 | + |
| 29 | +router = APIRouter( |
| 30 | + prefix="/api/reports", |
| 31 | + tags=["reports"], |
| 32 | + dependencies=[Depends(require_internal_token)], |
| 33 | +) |
| 34 | + |
| 35 | + |
| 36 | +@router.post("/mini", response_model=AiMiniReportResponse, summary="미니 리포트 생성") |
| 37 | +async def create_mini_report(req: AiReportRequest) -> AiMiniReportResponse: |
| 38 | + return AiMiniReportResponse( |
| 39 | + activity_summary=( |
| 40 | + f"{req.nickname}님은 KOPLE, 밋업 프로젝트 등 복수의 프로젝트를 동시에 진행하며 " |
| 41 | + "각 과제를 구조화하는 작업을 반복해왔어요. 특히 복잡한 요구사항이 주어졌을 때 " |
| 42 | + "먼저 전체 흐름을 정의하고 실행하는 방식이 기록 전반에서 일관되게 나타나요. " |
| 43 | + "설계 중 의문이 생기면 진행을 멈추고 재검토하는 패턴도 눈에 띄어요." |
| 44 | + ), |
| 45 | + next_focus_point=( |
| 46 | + "아직 협업·조율 영역 기록이 적어요. 팀원과 의견을 조율했던 경험이나 " |
| 47 | + "피드백을 주고받은 순간을 기록해보면 더 입체적인 커리어 서사가 만들어질 거예요." |
| 48 | + ), |
| 49 | + competency_frequency=competency_frequency(req.records), |
| 50 | + top_detail_tags=top_detail_tags(req.records), |
| 51 | + ) |
| 52 | + |
| 53 | + |
| 54 | +@router.post( |
| 55 | + "/career/branding", response_model=AiCareerBrandingResponse, summary="커리어 브랜딩 생성" |
| 56 | +) |
| 57 | +async def create_career_branding(req: AiReportRequest) -> AiCareerBrandingResponse: |
| 58 | + return AiCareerBrandingResponse( |
| 59 | + branding_statement=( |
| 60 | + f"{req.nickname}님은 복잡한 문제를 구조로 풀어내는 '설계형 기획자'입니다." |
| 61 | + ), |
| 62 | + branding_pattern="복잡한 상황에서 먼저 구조를 정의하는 행동 방식", |
| 63 | + top_detail_tags=top_detail_tag_stats(req.records), |
| 64 | + ) |
| 65 | + |
| 66 | + |
| 67 | +@router.post( |
| 68 | + "/career/narrative", response_model=AiCareerNarrativeResponse, summary="커리어 내러티브 생성" |
| 69 | +) |
| 70 | +async def create_career_narrative(req: AiReportRequest) -> AiCareerNarrativeResponse: |
| 71 | + return AiCareerNarrativeResponse( |
| 72 | + narrative_summary=( |
| 73 | + f"{req.nickname}님은 복수의 프로젝트를 동시에 진행하면서도 각 과제를 먼저 구조화하고 " |
| 74 | + "실행하는 방식을 일관되게 유지해왔어요. 기획 중 설계적 의문이 생기면 진행을 " |
| 75 | + "멈추고 재검토하는 패턴이 반복되며, 이는 완성도에 대한 높은 기준을 반영해요. " |
| 76 | + "#기획_구조화와 #구조_개선이 다른 역량 태그보다 유독 많이 나온 건 이 방식의 증거예요." |
| 77 | + ), |
| 78 | + ) |
| 79 | + |
| 80 | + |
| 81 | +@router.post( |
| 82 | + "/career/strengths-and-interview", |
| 83 | + response_model=AiCareerStrengthsAndInterviewResponse, |
| 84 | + summary="커리어 강점 및 면접 질문 생성", |
| 85 | +) |
| 86 | +async def create_career_strengths_and_interview( |
| 87 | + req: AiReportRequest, |
| 88 | +) -> AiCareerStrengthsAndInterviewResponse: |
| 89 | + record_ids = [r.star_record_id for r in req.records[:2]] |
| 90 | + evidences = build_evidences(req, record_ids) |
| 91 | + return AiCareerStrengthsAndInterviewResponse( |
| 92 | + strengths=[ |
| 93 | + StrengthItem( |
| 94 | + title="구조 먼저 잡는 기획력", |
| 95 | + description=( |
| 96 | + "요구사항이 복잡할수록 전체 흐름을 먼저 정의하고 실행해요. " |
| 97 | + "설계 중 의문이 생기면 멈추고 재검토하는 패턴이 일관돼요." |
| 98 | + ), |
| 99 | + evidences=evidences, |
| 100 | + ), |
| 101 | + StrengthItem( |
| 102 | + title="시스템으로 해결하는 협업", |
| 103 | + description=( |
| 104 | + "커뮤니케이션 문제를 개인 노력이 아닌 구조와 규칙으로 풀려는 접근이 반복돼요." |
| 105 | + ), |
| 106 | + evidences=evidences, |
| 107 | + ), |
| 108 | + ], |
| 109 | + interview_questions=[ |
| 110 | + InterviewQuestion( |
| 111 | + question=( |
| 112 | + "KOPLE 프로젝트에서 단기간에 완성했다고 하셨는데, " |
| 113 | + "구체적으로 얼마 만이었고 어떤 트레이드오프가 있었나요?" |
| 114 | + ), |
| 115 | + evidences=evidences, |
| 116 | + ), |
| 117 | + InterviewQuestion( |
| 118 | + question="진행을 멈추고 재검토하는 판단을 팀원들에게 어떻게 설득하셨나요?", |
| 119 | + evidences=evidences, |
| 120 | + ), |
| 121 | + ], |
| 122 | + ) |
| 123 | + |
| 124 | + |
| 125 | +@router.post( |
| 126 | + "/career/highlights", |
| 127 | + response_model=AiCareerHighlightsResponse, |
| 128 | + summary="커리어 하이라이트 생성", |
| 129 | +) |
| 130 | +async def create_career_highlights(_req: AiReportRequest) -> AiCareerHighlightsResponse: |
| 131 | + return AiCareerHighlightsResponse( |
| 132 | + experience_highlights=[ |
| 133 | + ( |
| 134 | + "복수의 이해관계자 요구가 충돌하는 KOPLE 프로젝트에서, " |
| 135 | + "전체 흐름을 구조화해 PRD와 기능명세서를 단기간에 완성했습니다." |
| 136 | + ), |
| 137 | + ( |
| 138 | + "설계 중 구조적 의문이 생겼을 때 진행을 멈추고 재검토함으로써, " |
| 139 | + "개발 착수 전 핵심 설계 이슈를 선제 해결했습니다." |
| 140 | + ), |
| 141 | + ] |
| 142 | + ) |
0 commit comments