Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f243bfa
feat(web): 코드 반영 탭 레이아웃 추가
ggamnunq Aug 14, 2026
98680c4
feat(web): 코드 반영 탭 구현
ggamnunq Aug 14, 2026
3c0066a
fix(web): 커밋 카드 선택 상태 정리
ggamnunq Aug 14, 2026
70ba156
refactor(web): 코드 반영 디자인 시스템 정리
ggamnunq Aug 14, 2026
6d73de7
fix(web): 코드 반영 커밋 상태 동기화
ggamnunq Aug 14, 2026
7fe76f8
refactor(web): 코드 반영 목록 책임 분리
ggamnunq Aug 14, 2026
48afe18
feat(server): 추천·연결 커밋 응답에 작성자·라인 수 추가
ggamnunq Aug 14, 2026
ccf27d6
fix(web): 코드 반영 탭 커밋 매핑·에러 노출 정리
ggamnunq Aug 14, 2026
6bea047
docs(docs): PR-14 리뷰 판단 근거 기록
whylog-dev Aug 15, 2026
e757018
feat(web): 원문 맥락 탭 구현
ggamnunq Aug 21, 2026
f2087aa
style(web): 원문 맥락 상세 정보 정리
ggamnunq Aug 21, 2026
592c427
chore: 원문 맥락 검증 데이터 확장
ggamnunq Aug 21, 2026
3cb0bae
feat(ai): 적용사항 발화별 요약(context_items) 추출 추가
ggamnunq Aug 21, 2026
81fbdcc
feat(server): 원문 맥락 발화별 요약 저장·조회 추가
ggamnunq Aug 21, 2026
1b25424
feat(web): 원문 맥락 탭에 발화별 요약 표시
ggamnunq Aug 21, 2026
e9c2b32
Merge remote-tracking branch 'origin/feat/application-tabs'
ggamnunq Aug 21, 2026
8de62c1
docs(docs): PR-14 리뷰 판단 근거 기록
whylog-dev Aug 21, 2026
37940cf
feat(web): 결정 관계도 추가
ggamnunq Aug 21, 2026
9ece492
feat: 적용사항 개요와 코드 반영 개선
ggamnunq Sep 5, 2026
7a8a125
Merge remote-tracking branch 'origin/develop' into feat/application-tabs
ggamnunq Sep 6, 2026
f8eaf65
docs(docs): PR-14 리뷰 판단 근거 기록
whylog-dev Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions ai/app/domains/meeting_analysis/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ class ApplicationTimelineItem(BaseModel):
utterance: str = Field(description="실제 발화 원문")


class ApplicationContextItem(BaseModel):
timestamp: str = Field(description="해당 발화 시각(HH:MM:SS)")
member_id: int | None = None
content: str = Field(description="해당 발화의 요약 한 문장")
utterance: str = Field(description="실제 발화 원문")


class Application(BaseModel):
application_id: int | None = Field(
default=None,
Expand All @@ -62,6 +69,13 @@ class Application(BaseModel):
default_factory=list,
description="적용사항 도출 타임라인(이슈제기→대안논의→적용합의)",
)
context_items: list[ApplicationContextItem] = Field(
default_factory=list,
description=(
"적용사항과 관련된 개별 발화별 요약 목록. "
"timeline과 달리 관련된 발화를 빠짐없이 담는다."
),
)


class MeetingAnalysisResult(BaseModel):
Expand Down
99 changes: 78 additions & 21 deletions ai/app/domains/meeting_analysis/services/extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from app.core.gemini import generate_content_with_retry
from app.domains.meeting_analysis.schemas import (
Application,
ApplicationContextItem,
ApplicationTimelineItem,
MeetingAnalysis,
MeetingAnalysisResult,
)
Expand Down Expand Up @@ -138,6 +140,15 @@
"4. timeline의 content는 간략한 한 문장으로 작성한다.",
*CONCRETE_APPLICATION_RULE,
*TIMELINE_EVIDENCE_ALIGNMENT_RULE,
"",
"[정책 4: 원문 맥락 정책]",
"1. context_items에는 해당 적용사항과 관련된 발화를 빠짐없이 담는다.",
" timeline의 대표 3단계와 달리 관련된 모든 발화를 포함하며,",
" timeline에 이미 들어간 발화도 다시 포함할 수 있다.",
"2. 각 항목은 실제 발화(utterance) 원문과 그 발화 하나에 대한",
" 간략한 한 문장 요약(content)을 함께 담는다.",
"3. member_id는 STT 데이터의 member_id 값을 사용하고, 없으면 null로 둔다.",
"4. 적용사항과 무관한 잡담·감정표현 발화는 포함하지 않는다.",
"------------------------------------------------------------",
"",
"[출력 JSON 구조]:",
Expand Down Expand Up @@ -166,6 +177,14 @@
' "content": "간략 요약 한 문장",',
' "utterance": "실제 발화 원문"',
" }",
" ],",
' "context_items": [',
" {",
' "timestamp": "...",',
' "member_id": 1,',
' "content": "해당 발화 요약 한 문장",',
' "utterance": "실제 발화 원문"',
" }",
" ]",
" }",
" ],",
Expand Down Expand Up @@ -233,6 +252,15 @@
*CONCRETE_APPLICATION_RULE,
*TIMELINE_EVIDENCE_ALIGNMENT_RULE,
"",
"[정책 4: 원문 맥락 정책]",
"1. context_items에는 해당 적용사항과 관련된 발화를 빠짐없이 담는다.",
" timeline의 대표 3단계와 달리 관련된 모든 발화를 포함하며,",
" timeline에 이미 들어간 발화도 다시 포함할 수 있다.",
"2. 각 항목은 실제 발화(utterance) 원문과 그 발화 하나에 대한",
" 간략한 한 문장 요약(content)을 함께 담는다.",
"3. member_id는 STT 데이터의 member_id 값을 사용하고, 없으면 null로 둔다.",
"4. 적용사항과 무관한 잡담·감정표현 발화는 포함하지 않는다.",
"",
"[출력 JSON 구조]",
"{",
' "applications": [',
Expand All @@ -250,6 +278,14 @@
' "content": "간략 요약 한 문장",',
' "utterance": "실제 발화 원문"',
" }",
" ],",
' "context_items": [',
" {",
' "timestamp": "...",',
' "member_id": 1,',
' "content": "해당 발화 요약 한 문장",',
' "utterance": "실제 발화 원문"',
" }",
" ]",
" }",
" ],",
Expand Down Expand Up @@ -568,45 +604,66 @@ def _infer_timeline_member_id(
return None


def _normalize_items_member_ids(
items: list[ApplicationTimelineItem | ApplicationContextItem],
segments: list[TranscribeSegment],
valid_member_ids: set[int],
) -> tuple[int, int]:
# LLM이 생성한 발화 항목의 member_id를 전사 세그먼트 기준으로 검증/보정
corrected_count = 0
unresolved_count = 0

for item in items:
if item.member_id in valid_member_ids:
continue

inferred = None
if valid_member_ids:
inferred = _infer_timeline_member_id(
utterance=item.utterance,
timestamp=item.timestamp,
segments=segments,
)
if inferred is not None:
item.member_id = inferred
corrected_count += 1
else:
# 추정이 불가능하면 임의 member_id를 만들지 않고 None으로 둔다.
item.member_id = None
unresolved_count += 1

return corrected_count, unresolved_count


def _normalize_timeline_member_ids(
result: MeetingAnalysisResult,
segments: list[TranscribeSegment],
) -> None:
# LLM이 생성한 member_id를 전사 세그먼트 기준으로 검증/보정
# LLM이 생성한 timeline/context_items의 member_id를 전사 세그먼트 기준으로 검증/보정
valid_member_ids = {
segment.member_id for segment in segments if segment.member_id is not None
}
corrected_count = 0
unresolved_count = 0

for application in result.applications:
for item in application.timeline:
if item.member_id in valid_member_ids:
continue

inferred = None
if valid_member_ids:
inferred = _infer_timeline_member_id(
utterance=item.utterance,
timestamp=item.timestamp,
segments=segments,
)
if inferred is not None:
item.member_id = inferred
corrected_count += 1
else:
# 추정이 불가능하면 임의 member_id를 만들지 않고 None으로 둔다.
item.member_id = None
unresolved_count += 1
for items in (application.timeline, application.context_items):
corrected, unresolved = _normalize_items_member_ids(
items,
segments,
valid_member_ids,
)
corrected_count += corrected
unresolved_count += unresolved

if corrected_count > 0:
logger.warning(
"Normalized %s invalid timeline member_id values.",
"Normalized %s invalid application item member_id values.",
corrected_count,
)
if unresolved_count > 0:
logger.warning(
"Could not infer %s timeline member_id values.",
"Could not infer %s application item member_id values.",
unresolved_count,
)

Expand Down
35 changes: 35 additions & 0 deletions ai/tests/test_timeline_member_id.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from app.domains.meeting_analysis.schemas import (
Application,
ApplicationContextItem,
ApplicationTimelineItem,
MeetingAnalysisResult,
)
Expand All @@ -16,8 +17,42 @@ def test_timeline_prompt_uses_member_id_not_legacy_speaker_field(self):
prompt = f"{APPLICATION_POLICY_PROMPT}\n{APPLICATIONS_ONLY_PROMPT}"

assert '"member_id": 1' in prompt
assert '"context_items"' in prompt
assert "speaker" + "_id" not in prompt

def test_context_item_member_id_is_inferred_from_transcript_segment(self):
result = MeetingAnalysisResult(
applications=[
Application(
application_title="Swagger 에러 응답 예시 문서화",
application_reasons=[],
context_items=[
ApplicationContextItem(
timestamp="00:00:03",
member_id=None,
content="ApiErrorCodeExample 추가 제안",
utterance="ApiErrorCodeExample을 추가하는 걸로 하죠.",
)
],
)
]
)
segments = [
TranscribeSegment(
message_id=1,
speaker="김준용",
member_id=7,
start_time="00:00:00",
end_time="00:00:05",
text="ApiErrorCodeExample을 추가하는 걸로 하죠.",
is_final=True,
)
]

_normalize_timeline_member_ids(result, segments)

assert result.applications[0].context_items[0].member_id == 7

def test_timeline_member_id_is_inferred_from_transcript_segment(self):
result = MeetingAnalysisResult(
applications=[
Expand Down
39 changes: 39 additions & 0 deletions docs/pr-reviews/PR-14.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# PR-14 AI 리뷰 기록

- PR: https://github.com/WhyLog-App/WhyLog/pull/14
- 제목: feat(web, server): 적용사항 대시보드 개선
- 브랜치: `develop` ← `feat/application-tabs`
- HEAD: `7a8a12553bc0452ed846c0a95fddf0674df8ad2e`
- 입력 digest: `331ddbd2b03e678f5b7f0172c3769a608e053d63bbf96605d972167be21a1e6f`
- 모델: OpenAI `gpt-5.6-terra`
- 상태: **BLOCKED**
- 생성 시각(UTC): 2026-09-06T18:29:31+00:00

## 요약

적용사항 원문 맥락 저장·조회와 대시보드 탭 UI를 추가했습니다. 다만 Web 파트의 신규 라이브러리 추가 금지 규칙을 위반합니다.

## 이번 실행에서 새로 발견됨

|상태|구분|위치|제목|이유|근거|권장 처리|반영 HEAD|
|---|---|---|---|---|---|---|---|
|new|차단|web/package.json:18|신규 UI 라이브러리 추가를 제거하거나 사전 합의가 필요합니다|`@xyflow/react`를 새 의존성으로 추가했습니다. Web 가이드는 라이브러리 추가를 금지하며, 필요한 경우 회의 안건으로 올리고 코드에 바로 반영하지 않도록 명시합니다.|web/AGENTS.md — AI에게 시킬 때 주의점 4. 라이브러리 추가 금지|기존 의존성과 컴포넌트로 관계도를 구현하거나, 해당 라이브러리 도입을 회의에서 합의한 뒤 별도 승인된 변경으로 추가하세요.||

## 이전 실행부터 계속 남아있음

없음

## 현재까지 사라짐(자동 추정)

없음

## 실행 이력

|HEAD|상태|모델|전체|신규|계속|해결|시각|
|---|---|---|---:|---:|---:|---:|---|
|7a8a12553bc0|BLOCKED|OpenAI gpt-5.6-terra|1|1|||2026-09-06T18:29:31+00:00|
|e9c2b3298c49|PASS|Google gemini-3.6-flash|||||2026-08-21T20:30:19+00:00|
|ccf27d61531b|PASS|Google gemini-3.6-flash|||||2026-08-15T08:51:38+00:00|

<!-- whylog-ai-pr-review-state {"findings":[{"file":"web/package.json","fingerprint":"0c232c4a61d8c19afa7e8e30","kind":"blocking","line":18,"reason":"`@xyflow/react`를 새 의존성으로 추가했습니다. Web 가이드는 라이브러리 추가를 금지하며, 필요한 경우 회의 안건으로 올리고 코드에 바로 반영하지 않도록 명시합니다.","recommendation":"기존 의존성과 컴포넌트로 관계도를 구현하거나, 해당 라이브러리 도입을 회의에서 합의한 뒤 별도 승인된 변경으로 추가하세요.","rule_reference":"web/AGENTS.md — AI에게 시킬 때 주의점 4. 라이브러리 추가 금지","title":"신규 UI 라이브러리 추가를 제거하거나 사전 합의가 필요합니다"}],"head_sha":"7a8a12553bc0452ed846c0a95fddf0674df8ad2e","history":[{"generated_at":"2026-09-06T18:29:31+00:00","head_sha":"7a8a12553bc0452ed846c0a95fddf0674df8ad2e","model":"gpt-5.6-terra","new":1,"ongoing":0,"provider":"OpenAI","resolved":0,"status":"BLOCKED","total":1},{"generated_at":"2026-08-21T20:30:19+00:00","head_sha":"e9c2b3298c4954f27f3057b2e45dbe1f1bd96005","model":"gemini-3.6-flash","new":0,"ongoing":0,"provider":"Google","resolved":0,"status":"PASS","total":0},{"generated_at":"2026-08-15T08:51:38+00:00","head_sha":"ccf27d61531b05b5857697b0adfb939c9cc3f328","model":"gemini-3.6-flash","new":0,"ongoing":0,"provider":"Google","resolved":0,"status":"PASS","total":0}],"model":"gpt-5.6-terra","pr":{"author":"ggamnunq","base":"develop","head":"feat/application-tabs","number":14,"title":"feat(web, server): 적용사항 대시보드 개선","url":"https://github.com/WhyLog-App/WhyLog/pull/14"},"provider":"OpenAI","resolved":[],"review_input_digest":"331ddbd2b03e678f5b7f0172c3769a608e053d63bbf96605d972167be21a1e6f","schema":1,"status":"BLOCKED","summary":"적용사항 원문 맥락 저장·조회와 대시보드 탭 UI를 추가했습니다. 다만 Web 파트의 신규 라이브러리 추가 금지 규칙을 위반합니다."} -->
<!-- whylog-ai-pr-review-signature b3501e866bf4d8b511e4750a53e4d21ba16138ed3ae4895599c40022bfe3d8c3 -->
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package com.whylog.server.domain.decision.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.List;

public class ApplicationResponse {


@Getter
@NoArgsConstructor
@AllArgsConstructor
Expand Down Expand Up @@ -70,7 +68,6 @@ public static class DecisionTimelineItemDTO {

@Schema(description = "타임라인 내용", example = "장애 이슈 제기")
private String content;

}

@Getter
Expand All @@ -89,14 +86,19 @@ public static class DecisionContextItemDTO {
@Schema(description = "발화자 이름", example = "김주뇽", nullable = true)
private String memberName;

@Schema(description = "발화자 프로필 사진", example = "https://example.com/profile.jpg", nullable = true)
@Schema(
description = "발화자 프로필 사진",
example = "https://example.com/profile.jpg",
nullable = true)
private String profileImage;

@Schema(description = "발화 요약", example = "로그인 API 검증 로직 추가를 합의함", nullable = true)
private String content;

@Schema(description = "대화 내용", example = "아니 우리 이거 버그난다니까?!?@??@")
private String dialogueContent;
}


@Getter
@NoArgsConstructor
@AllArgsConstructor
Expand All @@ -109,7 +111,6 @@ public static class DecisionReasonItemDTO {

@Schema(description = "근거 내용", example = "운영복잡 우려로 보류")
private String title;

}

@Getter
Expand Down Expand Up @@ -159,6 +160,15 @@ public static class RecommendedCommitDTO {
@Schema(description = "커밋 메시지", example = "feat: API 구현")
private String message;

@Schema(description = "작성자 이름", example = "홍길동")
private String authorName;

@Schema(description = "추가된 라인 수", example = "120")
private Integer addedLines;

@Schema(description = "삭제된 라인 수", example = "30")
private Integer deletedLines;

@Schema(description = "추천 사유", example = "이 커밋은 관련된 이슈를 해결하는 커밋입니다.")
private String reason;

Expand Down Expand Up @@ -199,6 +209,15 @@ public static class ConnectedCommitDTO {
@Schema(description = "커밋 메시지", example = "feat: API 구현")
private String message;

@Schema(description = "작성자 이름", example = "홍길동")
private String authorName;

@Schema(description = "추가된 라인 수", example = "120")
private Integer addedLines;

@Schema(description = "삭제된 라인 수", example = "30")
private Integer deletedLines;

@Schema(description = "커밋 날짜", example = "2026-03-24T10:30:00")
private LocalDateTime committedDate;
}
Expand All @@ -216,5 +235,4 @@ public static class CommitConnectionResponseDTO {
@Schema(description = "연결된 커밋 ID 목록", example = "[1, 2, 3]")
private List<Long> commitIds;
}

}
Loading
Loading