Skip to content

Commit 0284dd5

Browse files
committed
Merge branch 'main' into feat/#7-tagging-v1
2 parents 3d4459e + f36ec0e commit 0284dd5

13 files changed

Lines changed: 753 additions & 5 deletions

File tree

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ LOG_LEVEL=INFO
1414
# 생성: python -c "import secrets; print(secrets.token_urlsafe(32))"
1515
INTERNAL_API_TOKEN=change-me-to-a-random-32-byte-token
1616

17+
# ---- Feature variants ----
18+
# 리포트 생성 서비스의 활성 프롬프트 버전 (app/services/report/ 하위)
19+
REPORT_VARIANT=v1_baseline
20+
1721
# ---- AI provider ----
1822
ANTHROPIC_API_KEY=sk-ant-
1923
ANTHROPIC_MODEL=claude-haiku-4-5-20251001

app/api/report_public.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
)

app/api/v1/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from fastapi import APIRouter, Depends
1111

12-
from app.api.v1 import ping, tagging
12+
from app.api.v1 import ping, report, tagging
1313
from app.core.security import require_internal_token
1414

1515
router = APIRouter(
@@ -20,3 +20,4 @@
2020
# Feature 라우터 등록 — 새 기능은 한 줄씩 여기에 추가.
2121
router.include_router(ping.router)
2222
router.include_router(tagging.router)
23+
router.include_router(report.router)

app/api/v1/report.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""리포트 생성 실험 라우터 — ``/v1/reports/*``.
2+
3+
실제 서비스 레이어를 호출하는 실험·개발 경로.
4+
검증이 끝나면 ``/api/reports/*`` 핸들러 본문을 이쪽 service 호출로 교체하면 운영 전환 완료.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from fastapi import APIRouter
10+
11+
from app.schemas.report import (
12+
AiCareerBrandingResponse,
13+
AiCareerHighlightsResponse,
14+
AiCareerNarrativeResponse,
15+
AiCareerStrengthsAndInterviewResponse,
16+
AiMiniReportResponse,
17+
AiReportRequest,
18+
)
19+
from app.services import report as report_service
20+
21+
router = APIRouter(prefix="/reports", tags=["reports"])
22+
23+
24+
@router.post("/mini", response_model=AiMiniReportResponse, summary="미니 리포트 생성")
25+
async def create_mini_report(req: AiReportRequest) -> AiMiniReportResponse:
26+
return await report_service.run_mini(req)
27+
28+
29+
@router.post(
30+
"/career/branding", response_model=AiCareerBrandingResponse, summary="커리어 브랜딩 생성"
31+
)
32+
async def create_career_branding(req: AiReportRequest) -> AiCareerBrandingResponse:
33+
return await report_service.run_branding(req)
34+
35+
36+
@router.post(
37+
"/career/narrative", response_model=AiCareerNarrativeResponse, summary="커리어 내러티브 생성"
38+
)
39+
async def create_career_narrative(req: AiReportRequest) -> AiCareerNarrativeResponse:
40+
return await report_service.run_narrative(req)
41+
42+
43+
@router.post(
44+
"/career/strengths-and-interview",
45+
response_model=AiCareerStrengthsAndInterviewResponse,
46+
summary="커리어 강점 및 면접 질문 생성",
47+
)
48+
async def create_career_strengths_and_interview(
49+
req: AiReportRequest,
50+
) -> AiCareerStrengthsAndInterviewResponse:
51+
return await report_service.run_strengths_and_interview(req)
52+
53+
54+
@router.post(
55+
"/career/highlights",
56+
response_model=AiCareerHighlightsResponse,
57+
summary="커리어 하이라이트 생성",
58+
)
59+
async def create_career_highlights(req: AiReportRequest) -> AiCareerHighlightsResponse:
60+
return await report_service.run_highlights(req)

app/core/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ class Settings(BaseSettings):
5555
),
5656
)
5757

58+
# ---- Feature variants ----
59+
report_variant: str = Field(
60+
default="v1_baseline",
61+
description="리포트 생성 서비스의 활성 구현체 버전 (app/services/report/ 하위).",
62+
)
63+
5864
# ---- AI provider ----
5965
anthropic_api_key: str = Field(
6066
default="dev-no-api-key",

app/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from fastapi import FastAPI
1616

1717
from app import __version__
18-
from app.api import health, tagging_public
18+
from app.api import health, report_public, tagging_public
1919
from app.api.v1 import router as v1_router
2020
from app.core.config import get_settings
2121
from app.core.logging import configure_logging, get_logger
@@ -74,10 +74,12 @@ def create_app() -> FastAPI:
7474
# include_router 목록에 한 줄 추가하는 식으로 붙인다 (여기는 건드리지 않음).
7575
# - tagging_public: Spring 합의 안정 경로 /api/tagging. 현재 더미 응답이며
7676
# v1_baseline 검증 후 cutover 시 핸들러 본문을 service 호출로 교체한다.
77+
# - report_public: Spring 합의 안정 경로 /api/reports/*. 현재 더미 응답.
7778
# CORS 미들웨어는 의도적으로 미포함 — 서버 간 통신이므로 불필요.
7879
app.add_middleware(RequestContextMiddleware)
7980

8081
app.include_router(health.router)
82+
app.include_router(report_public.router)
8183
app.include_router(v1_router)
8284
app.include_router(tagging_public.router)
8385

app/schemas/common.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ class JobRole(StrEnum):
1717
DESIGNER = "DESIGNER"
1818

1919

20+
class UserStatus(StrEnum):
21+
"""유저 상태 — Spring 측 enum 코드와 동일."""
22+
23+
STUDENT = "STUDENT"
24+
JOB_SEEKER = "JOB_SEEKER"
25+
EMPLOYED = "EMPLOYED"
26+
27+
2028
class PrimaryCategory(StrEnum):
2129
"""5대 직무 역량 — Spring 측 enum 코드와 동일."""
2230

0 commit comments

Comments
 (0)