@@ -373,7 +418,7 @@ const props = defineProps({
record: { type: Object, default: null }
});
-const emit = defineEmits(['scroll-to-line', 'acorn-used']);
+const emit = defineEmits(['scroll-to-line', 'acorn-used', 'analysis-loaded']);
const { user } = useAuth();
@@ -396,16 +441,30 @@ const tutorMessages = ref([]);
const loadingTutorResponse = ref(false);
const chatContainer = ref(null);
const tutorInputRef = ref(null);
+const mergedRecord = ref(null);
+const analysisStatus = ref('idle'); // idle | loading | ready | error
+const analysisError = ref('');
+const currentAnalysisRequestId = ref(0);
+const record = computed(() => mergedRecord.value);
// Reset when record changes
-watch(() => props.record, (newRecord) => {
- if (newRecord) {
+watch(
+ () => props.record,
+ async (newRecord) => {
activeTab.value = 'overview';
tutorMessages.value = [];
showTrace.value = false;
showComplexityExplanation.value = false;
-
- // Initialize AI Data if exists
+ analysisError.value = '';
+
+ if (!newRecord) {
+ mergedRecord.value = null;
+ aiData.value = null;
+ analysisStatus.value = 'idle';
+ return;
+ }
+
+ mergedRecord.value = { ...newRecord };
if (newRecord.counterExampleInput) {
aiData.value = {
input: newRecord.counterExampleInput,
@@ -416,22 +475,118 @@ watch(() => props.record, (newRecord) => {
} else {
aiData.value = null;
}
- } else {
- aiData.value = null;
- }
+
+ if (hasExistingAnalysis(newRecord)) {
+ analysisStatus.value = 'ready';
+ return;
+ }
+
+ await ensureAnalysisLoaded();
+ },
+ { immediate: true }
+);
+
+const hasExistingAnalysis = (recordLike) => {
+ if (!recordLike) return false;
+ return Boolean(
+ recordLike.summary ||
+ recordLike.timeComplexity ||
+ recordLike.spaceComplexity ||
+ recordLike.fullResponse ||
+ recordLike.pitfalls ||
+ recordLike.algorithmIntuition ||
+ recordLike.keyBlocks ||
+ recordLike.refactorProvided
+ );
+};
+
+const mapAnalysisToRecord = (analysis) => ({
+ score: analysis.score ?? null,
+ timeComplexity: analysis.timeComplexity ?? null,
+ spaceComplexity: analysis.spaceComplexity ?? null,
+ complexityExplanation: analysis.complexityExplanation ?? null,
+ patterns: analysis.patterns ?? null,
+ algorithmIntuition: analysis.algorithmIntuition ?? null,
+ pitfalls: analysis.pitfalls ?? null,
+ keyBlocks: analysis.keyBlocks ?? null,
+ refactorProvided: analysis.refactorProvided ?? false,
+ refactorCode: analysis.refactorCode ?? null,
+ refactorExplanation: analysis.refactorExplanation ?? null,
+ fullResponse: analysis.fullResponse ?? null
});
+const applyAnalysisToRecord = (analysis) => {
+ const nextRecord = { ...record.value, ...mapAnalysisToRecord(analysis || {}) };
+ mergedRecord.value = nextRecord;
+ emit('analysis-loaded', nextRecord);
+ return nextRecord;
+};
+
+const ensureAnalysisLoaded = async ({ force = false } = {}) => {
+ if (!record.value?.id) return;
+ const requestId = ++currentAnalysisRequestId.value;
+ analysisStatus.value = 'loading';
+ analysisError.value = '';
+
+ try {
+ let analysis = null;
+
+ if (!force) {
+ const existing = await aiApi.getAnalysisResult(record.value.id);
+ const existingData = existing?.data ?? null;
+ analysis = hasExistingAnalysis(existingData) ? existingData : null;
+ }
+
+ if (!analysis) {
+ const created = await aiApi.analyzeCode({
+ algorithmRecordId: record.value.id,
+ force
+ });
+ analysis = created?.data ?? null;
+ }
+
+ if (requestId !== currentAnalysisRequestId.value) return;
+ applyAnalysisToRecord(analysis);
+ analysisStatus.value = 'ready';
+ } catch (error) {
+ if (requestId !== currentAnalysisRequestId.value) return;
+
+ if (error?.response?.status === 404 && !force) {
+ try {
+ const created = await aiApi.analyzeCode({
+ algorithmRecordId: record.value.id
+ });
+ if (requestId !== currentAnalysisRequestId.value) return;
+ applyAnalysisToRecord(created?.data || {});
+ analysisStatus.value = 'ready';
+ return;
+ } catch (createError) {
+ analysisError.value = createError?.response?.data?.message || '분석 생성 요청에 실패했습니다.';
+ analysisStatus.value = 'error';
+ return;
+ }
+ }
+
+ analysisError.value = error?.response?.data?.message || '분석 조회에 실패했습니다.';
+ analysisStatus.value = 'error';
+ }
+};
+
+const retryAnalysis = async () => {
+ await ensureAnalysisLoaded({ force: true });
+};
+
// Computed Properties (Parsing)
const parsedFullResponse = computed(() => {
- if (!props.record?.fullResponse) return null;
- try { return JSON.parse(props.record.fullResponse); } catch { return null; }
+ if (!record.value?.fullResponse) return null;
+ try { return JSON.parse(record.value.fullResponse); } catch { return null; }
});
const fetchTutorHistory = async () => {
- if (!props.record?.id || !user.value?.id) return;
+ if (!record.value?.id || !user.value?.id) return;
try {
- const res = await aiApi.getTutorHistory(props.record.id, user.value.id);
+ const res = await aiApi.getTutorHistory(record.value.id, user.value.id);
if (Array.isArray(res.data)) {
tutorMessages.value = res.data.map(m => ({
role: m.role,
@@ -460,34 +615,47 @@ const parsedVariables = computed(() => parsedStructure.value.filter(item => !ite
const parsedFunctions = computed(() => parsedStructure.value.filter(item => item.type === 'function' || item.type === 'class'));
const parsedKeyBlocks = computed(() => {
- if (!props.record?.keyBlocks) return [];
- try { return Array.isArray(JSON.parse(props.record.keyBlocks)) ? JSON.parse(props.record.keyBlocks) : []; } catch { return []; }
+ if (!record.value?.keyBlocks) return [];
+ try { return Array.isArray(JSON.parse(record.value.keyBlocks)) ? JSON.parse(record.value.keyBlocks) : []; } catch { return []; }
});
const parsedSummary = computed(() => parsedFullResponse.value?.summary || null);
const parsedTraceExample = computed(() => parsedFullResponse.value?.traceExample || null);
-const parsedIntuition = computed(() => parsedFullResponse.value?.algorithm?.intuition || props.record?.algorithmIntuition || null);
+const parsedIntuition = computed(() => parsedFullResponse.value?.algorithm?.intuition || record.value?.algorithmIntuition || null);
const parsedPitfalls = computed(() => {
- if (!parsedFullResponse.value || !parsedFullResponse.value.pitfalls) return [];
- return Array.isArray(parsedFullResponse.value.pitfalls) ? parsedFullResponse.value.pitfalls : [];
+ const fullResponsePitfalls = parsedFullResponse.value?.pitfalls;
+ if (Array.isArray(fullResponsePitfalls)) {
+ return fullResponsePitfalls;
+ }
+
+ if (record.value?.pitfalls) {
+ try {
+ const parsed = JSON.parse(record.value.pitfalls);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch {
+ return [];
+ }
+ }
+
+ return [];
});
const hasAnyAnalysis = computed(() => {
- return props.record?.timeComplexity ||
- props.record?.spaceComplexity ||
+ return record.value?.timeComplexity ||
+ record.value?.spaceComplexity ||
parsedPitfalls.value.length > 0 ||
- props.record?.refactorProvided ||
+ record.value?.refactorProvided ||
parsedVariables.value.length > 0 ||
parsedSummary.value ||
parsedIntuition.value;
});
const isPassed = computed(() => {
- if (!props.record) return false;
- // runtimeMs가 null이거나 -1이면 실패로 처리
- const runtime = props.record.runtimeMs;
- return props.record.result === 'SUCCESS' || props.record.result === 'PASSED' || (runtime !== null && runtime !== undefined && runtime !== -1);
+ if (!record.value) return false;
+ // runtimeMs媛 null?닿굅??-1?대㈃ ?ㅽ뙣濡?泥섎━
+ const runtime = record.value.runtimeMs;
+ return record.value.result === 'SUCCESS' || record.value.result === 'PASSED' || (runtime !== null && runtime !== undefined && runtime !== -1);
});
// ACTIONS
@@ -498,8 +666,8 @@ const emitScrollToLine = (start, end) => {
};
const viewCodeLine = (name) => {
- if (!name || !props.record?.code) return;
- const lines = props.record.code.split('\n');
+ if (!name || !record.value?.code) return;
+ const lines = record.value.code.split('\n');
// Handle comma-separated names like "N, K" - search for each part
const searchTerms = name.split(',').map(s => s.trim()).filter(s => s.length > 0);
@@ -552,12 +720,12 @@ const findCounterExample = async () => {
loadingAi.value = true;
try {
const res = await aiApi.generateCounterExample({
- recordId: props.record.id,
- problemNumber: String(props.record.problemNumber),
- code: props.record.code,
- language: props.record.language,
- platform: props.record.platform,
- problemTitle: props.record.title
+ recordId: record.value.id,
+ problemNumber: String(record.value.problemNumber),
+ code: record.value.code,
+ language: record.value.language,
+ platform: record.value.platform,
+ problemTitle: record.value.title
});
aiData.value = res.data;
} catch (e) {
@@ -573,60 +741,48 @@ const sendTutorMessage = async () => {
const userMsg = tutorInput.value;
tutorMessages.value.push({ role: 'user', content: userMsg });
tutorInput.value = '';
-
- // Add temporary loading message
const loadingMsg = { role: 'assistant', content: '', isLoading: true };
tutorMessages.value.push(loadingMsg);
loadingTutorResponse.value = true;
-
- // Auto-scroll
- nextTick(() => { if(chatContainer.value) chatContainer.value.scrollTop = chatContainer.value.scrollHeight; });
-
- // History excludes current & loading
- // We already pushed userMsg and loadingMsg, so slice -2 to get history before this turn
- const history = tutorMessages.value.slice(0, -2).map(m => ({
- role: m.role === 'user' ? 'user' : 'assistant',
- content: m.content
+ nextTick(() => { if (chatContainer.value) chatContainer.value.scrollTop = chatContainer.value.scrollHeight; });
+
+ const history = tutorMessages.value.slice(0, -2).map(m => ({
+ role: m.role === 'user' ? 'user' : 'assistant',
+ content: m.content
}));
try {
const res = await aiApi.tutorChat({
userId: user.value?.id,
- recordId: props.record.id,
+ recordId: record.value.id,
message: userMsg,
solveStatus: isPassed.value ? 'solved' : 'wrong',
- wrongReason: !isPassed.value ? '틀렸습니다' : null,
- history: history
+ wrongReason: !isPassed.value ? '오답입니다' : null,
+ history
});
- // Remove loading message
tutorMessages.value.pop();
-
tutorMessages.value.push({
role: 'assistant',
- content: res.data.reply || res.data.answer || "답변을 생성할 수 없습니다.",
+ content: res.data.reply || res.data.answer || '응답을 생성하지 못했습니다.',
suggestions: res.data.followUpQuestions,
concepts: res.data.relatedConcepts,
encouragement: res.data.encouragement
});
-
+
emit('acorn-used');
} catch (e) {
console.error("Tutor chat failed", e);
-
- // Remove loading message
tutorMessages.value.pop();
-
const errorMsg = e.response?.data?.message || '';
if (errorMsg.includes('Not enough acorns')) {
- tutorMessages.value.push({ role: 'assistant', content: "도토리가 부족해 응답을 생성할 수 없습니다." });
+ tutorMessages.value.push({ role: 'assistant', content: '도토리가 부족해 응답을 생성할 수 없습니다.' });
} else {
- tutorMessages.value.push({ role: 'assistant', content: "죄송합니다, 답변을 생성하는 중 오류가 발생했습니다." });
+ tutorMessages.value.push({ role: 'assistant', content: '죄송합니다. 응답 생성 중 오류가 발생했습니다.' });
}
} finally {
loadingTutorResponse.value = false;
- // Scroll to bottom after response
- nextTick(() => { if(chatContainer.value) chatContainer.value.scrollTop = chatContainer.value.scrollHeight; });
+ nextTick(() => { if (chatContainer.value) chatContainer.value.scrollTop = chatContainer.value.scrollHeight; });
}
};
diff --git a/frontend/src/views/dashboard/DashboardRecordCard.vue b/frontend/src/views/dashboard/DashboardRecordCard.vue
index adacb353..0492b5de 100644
--- a/frontend/src/views/dashboard/DashboardRecordCard.vue
+++ b/frontend/src/views/dashboard/DashboardRecordCard.vue
@@ -1,485 +1,317 @@
-
-
-
-
+
-
-
- {{ isPassed ? 'SUCCESS' : 'FAILED' }}
+
+
+ {{ isPassed ? 'SUCCESS' : 'FAILED' }}
-
-
+
-
-
-
-
+
-
-
-
-
- {{ defenseStreak }}연승
-
-
-
- {{ formatElapsedTime(props.record.elapsedTimeSeconds) }}
-
+
+
+ {{ defenseStreak }}연속
+
+
+ {{ formatElapsedTime(record.elapsedTimeSeconds) }}
+
-
-
+
-
-
-
+
+
+
+ {{ formatDate(record.createdAt) }}
+
+
+
+
+
+
+
+
+
+
+ #{{ record.problemNumber }}
+
+
-
-
+ {{ record.language }}
+
+
+ {{ platformBadge }}
+
+
+ {{ hasAnyAnalysis ? '분석 완료' : '클릭 시 AI 분석 생성' }}
+
+
-
- {{ formatDate(record.createdAt) }}
+
+ {{ record.title }}
+
-
-
-
-
-
-
-
-
-
-
-
- #{{ record.problemNumber }}
-
-
- {{ record.language }}
-
-
-
-
- {{ platformBadge }}
-
-
-
-
-
- {{ record.title }}
-
-
-
-
-
-
+
+
+
+
-
-
-
@@ -487,13 +319,4 @@ defineExpose({ scrollToLine });
diff --git a/frontend/src/views/dashboard/DashboardView.vue b/frontend/src/views/dashboard/DashboardView.vue
index 7d182cd9..5f8501c8 100644
--- a/frontend/src/views/dashboard/DashboardView.vue
+++ b/frontend/src/views/dashboard/DashboardView.vue
@@ -525,7 +525,7 @@
@@ -1009,6 +1009,22 @@ const handleScrollToLine = ({ start, end }) => {
}
};
+const handleAnalysisLoaded = (nextRecord) => {
+ if (!nextRecord?.id) return;
+
+ records.value = records.value.map(record => (
+ record.id === nextRecord.id ? { ...record, ...nextRecord } : record
+ ));
+
+ if (activeAnalysisRecord.value?.id === nextRecord.id) {
+ activeAnalysisRecord.value = { ...activeAnalysisRecord.value, ...nextRecord };
+ }
+
+ if (currentDrawerRecord.value?.id === nextRecord.id) {
+ currentDrawerRecord.value = { ...currentDrawerRecord.value, ...nextRecord };
+ }
+};
+
const acornLogs = ref([]);
const missions = ref([]);
diff --git a/frontend/src/views/onboarding/OnboardingStep4Extension.vue b/frontend/src/views/onboarding/OnboardingStep4Extension.vue
index d85a2334..4340dd3a 100644
--- a/frontend/src/views/onboarding/OnboardingStep4Extension.vue
+++ b/frontend/src/views/onboarding/OnboardingStep4Extension.vue
@@ -47,9 +47,9 @@
새로고침
설치하기
@@ -126,7 +126,6 @@
-
diff --git a/frontend/src/views/onboarding/OnboardingStep5Repo.vue b/frontend/src/views/onboarding/OnboardingStep5Repo.vue
index 67996ac5..efaf3f1b 100644
--- a/frontend/src/views/onboarding/OnboardingStep5Repo.vue
+++ b/frontend/src/views/onboarding/OnboardingStep5Repo.vue
@@ -12,22 +12,26 @@
저장소 확인
- 대시허브 익스텐션에 설정된 저장소를 감지합니다.
+ 대시허브 익스텐션에 설정된 저장소를 감지하고,
+ GitHub App을 통해 실시간 동기화를 연결합니다.
-
-
+
+
-
저장소 감지 중...
-
익스텐션 설정을 확인하고 있습니다.
+
+
저장소 감지 중...
+
익스텐션 설정을 확인하고 있습니다.
+
+
@@ -41,17 +45,79 @@
{{ detectedRepo.fullName }}
{{ detectedRepo.description || '설명 없음' }}
-
+
+
+
+
-
+
+
+
+
+
+ 재탐지 시도 후에도 감지되지 않는다면,
+ GitHub App 설치가 정상적으로 완료되었는지 다시 확인해주세요.
+
+
+
설치가 완료되면 아래 '다시 탐지하기' 버튼을 눌러주세요.
+