Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c2c87a7
feat: 이력 상세 화면에서 리스크 진단 결과를 DiagnosisDetailView로 표시
tmdcks1103 May 26, 2026
a5a6b1e
fix: 홈 대시보드 최근 분석 이력을 최신순 10개로 제한
tmdcks1103 May 26, 2026
14d31da
fix: 기능 없는 테이블 컬럼 정렬 화살표 제거
tmdcks1103 May 26, 2026
879a20d
fix: 기능 없는 요약 카드 화살표 제거
tmdcks1103 May 26, 2026
ed96756
fix: 엔진 미지원 공종도 Step4에 표시하고 선택 완료 안내 추가
tmdcks1103 May 26, 2026
93facd1
refactor: 가견적서 생성 사용자 입력 관련 리팩토링 (Step4 제거)
tmdcks1103 May 26, 2026
24c88ff
fix: 레이아웃에서 서버 유저를 클라이언트 인증 스토어에 동기화
tmdcks1103 May 26, 2026
7b4e488
fix: 가견적 결과 화면 참조 데이터 없는 공정 컬럼 숨김 및 버튼 정리
tmdcks1103 May 26, 2026
30a51d2
fix: 견적서 진단 결과 화면에서 기능 없는 PDF 다운로드 버튼 제거
tmdcks1103 May 26, 2026
d4a0bdf
feat: 분석 이력 페이지 목 데이터 제거 및 실제 API 연동
tmdcks1103 May 26, 2026
df4e83d
fix: 분석 이력 테이블 업체 컬럼 제거 및 리스크 표시 세분화
tmdcks1103 May 26, 2026
6b9b2c1
fix: AuthStoreInitializer에서 useEffect를 useLayoutEffect로 교체하여 auth 스토어…
tmdcks1103 May 26, 2026
d728ae6
fix: Next.js API 프록시 rewrites 추가
tmdcks1103 May 27, 2026
4eab0b3
fix: API 클라이언트 및 analyzeRisk 함수 수정
tmdcks1103 May 27, 2026
9b6d9ea
feat: 견적서 진단 리스크 집계 개선 및 오류 화면 다시 시도 버튼 추가
tmdcks1103 May 27, 2026
dfc99bc
refactor: 로딩 화면 공통 컴포넌트 추출
tmdcks1103 May 27, 2026
ec875e1
chore: main 브랜치와 병합 및 병합 중 충돌 해결
tmdcks1103 May 27, 2026
7786912
style : 로딩 화면 수정
tmdcks1103 May 27, 2026
c0dc13a
fix: 테스트 목 데이터 불일치 수정
tmdcks1103 May 27, 2026
3e472ef
fix: rewrites 제거
tmdcks1103 May 27, 2026
59d3f66
fix: beforeRetry 네트워크 오류 처리
tmdcks1103 May 28, 2026
9f8da39
fix: 고정 px 너비 반응형 처리
tmdcks1103 May 28, 2026
ec7bed1
fix: 접근성 누락 해결
tmdcks1103 May 28, 2026
173f826
fix: inferItemStatus 빈 title 미처리
tmdcks1103 May 28, 2026
64232db
fix: 부분 도배 미선택 시 통과
tmdcks1103 May 28, 2026
3da21c8
fix: summary.chips 직접 참조
tmdcks1103 May 28, 2026
47f3d66
fix: 불필요한 주석 삭제
tmdcks1103 May 28, 2026
2573882
refactor: LoadingScreen 반복·타이머 로직을 커스텀 훅으로 분리
tmdcks1103 May 28, 2026
c317d57
refactor: LoadingScreen UI를 역할별 하위 컴포넌트로 분리
tmdcks1103 May 28, 2026
6f86a43
refactor: LoadingScreen을 하위 컴포넌트 조립 레이어로 교체
tmdcks1103 May 28, 2026
564945f
refactor: Zustand authStore를 React Context(UserProvider)로 교체
tmdcks1103 May 28, 2026
2a4aff4
fix: 로딩 화면 위치 수정
tmdcks1103 May 28, 2026
81822e6
feat: 진단 요약을 카운트 배지에서 종합 인사이트 3문장으로 개선
tmdcks1103 May 28, 2026
294bd70
fix: useRotatingIndex에서 setInterval 콜백 내 setTimeout(tid) 미정리 버그 수정
tmdcks1103 May 28, 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
8 changes: 6 additions & 2 deletions apps/web/src/api/analyze/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { type RiskDetectAnalyzeResponse, type RiskSaveBody } from './types';

const isDev = process.env.NODE_ENV === 'development';

export const analyzeRisk = async (formData: FormData, signal?: AbortSignal): Promise<RiskDetectAnalyzeResponse> => {
export const analyzeRisk = async (
formData: FormData,
userId: number,
signal?: AbortSignal
): Promise<RiskDetectAnalyzeResponse> => {
if (isDev) {
console.group('[진단] analyzeRisk — POST /api/v1/risk-detector/analyze');
for (const [key, value] of formData.entries()) {
Expand All @@ -18,7 +22,7 @@ export const analyzeRisk = async (formData: FormData, signal?: AbortSignal): Pro
const res = await client
.post('api/v1/risk-detector/analyze', {
body: formData,
headers: { 'content-type': undefined },
headers: { 'content-type': undefined, 'x-user-id': String(userId) },
timeout: 180000,
signal,
})
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ const createClient = () =>
hooks: {
beforeRetry: [
async ({ request, error }) => {
const status = (error as { status?: number }).status;
if (status !== 401) {
throw error;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (isPreAuth(request.url) || isRefresh(request.url)) {
throw error;
}
Expand Down
256 changes: 256 additions & 0 deletions apps/web/src/app/(main)/_components/LoadingScreen/LoadingScreen.css.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

불필요한 주석은 삭제해주세요!!

Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import { vars } from '@muneo/design-system';
import { keyframes, style } from '@vanilla-extract/css';

const float = keyframes({
'0%, 100%': { transform: 'translateY(0px)' },
'50%': { transform: 'translateY(-12px)' },
});

const shadowPulse = keyframes({
'0%, 100%': { transform: 'scaleX(1)', opacity: 0.18 },
'50%': { transform: 'scaleX(0.72)', opacity: 0.08 },
});

const workingBounce = keyframes({
'0%, 80%, 100%': { transform: 'translateY(0)', opacity: 0.35 },
'40%': { transform: 'translateY(-6px)', opacity: 1 },
});

const msgIn = keyframes({
from: { opacity: 0, transform: 'translateY(8px)' },
to: { opacity: 1, transform: 'translateY(0)' },
});

const msgOut = keyframes({
from: { opacity: 1, transform: 'translateY(0)' },
to: { opacity: 0, transform: 'translateY(-6px)' },
});

const shimmer = keyframes({
from: { transform: 'translateX(-100%)' },
to: { transform: 'translateX(400%)' },
});

const cardIn = keyframes({
from: { opacity: 0, transform: 'translateX(24px)' },
to: { opacity: 1, transform: 'translateX(0)' },
});

const cardOut = keyframes({
from: { opacity: 1, transform: 'translateX(0)' },
to: { opacity: 0, transform: 'translateX(-24px)' },
});

const MOTION = '(prefers-reduced-motion: reduce)' as const;

export const container = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '60vh',
padding: '120px 24px 40px',
});

export const mascotWrap = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginBottom: '8px',
});

export const mascotFloating = style({
animation: `${float} 2.6s ease-in-out infinite`,
'@media': { [MOTION]: { animation: 'none' } },
});

export const mascotImg = style({
width: '92px',
height: 'auto',
display: 'block',
});

export const mascotShadow = style({
width: '56px',
height: '8px',
borderRadius: '50%',
background: 'rgba(132, 85, 223, 0.22)',
filter: 'blur(4px)',
marginTop: '2px',
animation: `${shadowPulse} 2.6s ease-in-out infinite`,
'@media': { [MOTION]: { animation: 'none' } },
});

export const workingDots = style({
display: 'flex',
gap: '6px',
justifyContent: 'center',
marginTop: '12px',
marginBottom: '20px',
});

export const workingDot = style({
width: '7px',
height: '7px',
borderRadius: '50%',
backgroundColor: vars.color.brand.primary,
opacity: 0.35,
animation: `${workingBounce} 1.4s ease-in-out infinite`,
'@media': { [MOTION]: { animation: 'none', opacity: 1 } },
});

export const msgWrap = style({
height: '22px',
display: 'flex',
alignItems: 'center',
marginBottom: '14px',
});

export const msg = style({
fontSize: vars.typography.fontSize.sm,
color: vars.color.neutral.n500,
textAlign: 'center',
fontWeight: vars.typography.fontWeight.medium,
animation: `${msgIn} 0.35s ease-out both`,
'@media': { [MOTION]: { animation: 'none' } },
});

export const msgExiting = style({
animation: `${msgOut} 0.25s ease-in both`,
'@media': { [MOTION]: { animation: 'none' } },
});

export const progressWrap = style({
width: '100%',
maxWidth: '320px',
height: '5px',
borderRadius: '10px',
backgroundColor: vars.color.neutral.n200,
overflow: 'hidden',
marginBottom: '32px',
position: 'relative',
});

export const progressFill = style({
height: '100%',
borderRadius: '10px',
background: 'linear-gradient(90deg, #7B5CE5 0%, #B199FF 100%)',
width: '0%',
willChange: 'width',
position: 'relative',
overflow: 'hidden',
selectors: {
'&::after': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
width: '25%',
height: '100%',
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.5), transparent)',
animation: `${shimmer} 2.2s ease-in-out infinite`,
},
},
'@media': {
[MOTION]: {
selectors: {
'&::after': { animation: 'none' },
},
},
},
});

export const tipCard = style({
width: '100%',
maxWidth: '480px',
boxSizing: 'border-box',
backgroundColor: '#F5F0FF',
border: '1px solid #E3D8FF',
borderRadius: '16px',
padding: '20px 24px 16px',
display: 'flex',
flexDirection: 'column',
gap: '10px',
marginBottom: '20px',
animation: `${cardIn} 0.4s cubic-bezier(0.22, 1, 0.36, 1) both`,
'@media': { [MOTION]: { animation: 'none' } },
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

export const tipCardExiting = style({
animation: `${cardOut} 0.28s ease-in both`,
'@media': { [MOTION]: { animation: 'none' } },
});

export const tipHeader = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
});

export const tipLabel = style({
display: 'flex',
alignItems: 'center',
gap: '5px',
fontSize: vars.typography.fontSize.xs,
fontWeight: vars.typography.fontWeight.semiBold,
color: '#7B5CE5',
letterSpacing: '0.02em',
});

export const tipCounter = style({
fontSize: vars.typography.fontSize.xs,
color: '#B199FF',
fontWeight: vars.typography.fontWeight.medium,
fontVariantNumeric: 'tabular-nums',
});

export const tipText = style({
fontSize: vars.typography.fontSize.base,
color: vars.color.neutral.n700,
lineHeight: '1.75',
fontWeight: vars.typography.fontWeight.regular,
wordBreak: 'keep-all',
});

export const tipProgressTrack = style({
width: '100%',
height: '3px',
borderRadius: '10px',
backgroundColor: '#E3D8FF',
overflow: 'hidden',
marginTop: '4px',
});

export const tipProgressFill = style({
height: '100%',
borderRadius: '10px',
background: 'linear-gradient(90deg, #8455DF, #B199FF)',
transition: 'width 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
});

export const footer = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '10px',
});

export const footerText = style({
fontSize: vars.typography.fontSize.xs,
color: vars.color.neutral.n400,
});

export const cancelBtn = style({
background: 'none',
border: `1px solid ${vars.color.neutral.n300}`,
borderRadius: vars.radius.sm,
padding: '8px 20px',
fontSize: vars.typography.fontSize.sm,
color: vars.color.neutral.n500,
cursor: 'pointer',
fontFamily: vars.typography.fontFamily,
transition: 'opacity 0.15s ease',
selectors: {
'&:hover': { opacity: 0.65 },
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

메시지 회전과 팁 회전이 동일 패턴 두 번 반복 되고 있는 것 같은데, useRotatingIndex(count, interval, exit) 커스텀 훅으로 추출하면 좋을 것 같아요!

LoadingScreen/
  LoadingScreen.tsx         ← 얇은 조립
  LoadingScreen.css.ts
  _components/
    Mascot.tsx              ← SVG 분리
    WorkingDots.tsx
    RotatingMessage.tsx     ← message + warning swap
    ProgressBar.tsx
    TipCard.tsx
    LoadingFooter.tsx
  _hooks/
    useRotatingIndex.ts     ← message/tip 공통 로직
    useProgressSteps.ts
    useDelayedFlag.ts       ← warning 노출

이런식으로 분리해서 로딩스크린 작성하면 좋을 것 같습니다! 😄

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { LoadingFooter } from './_components/LoadingFooter';
import { Mascot } from './_components/Mascot';
import { ProgressBar } from './_components/ProgressBar';
import { RotatingMessage } from './_components/RotatingMessage';
import { TipCard } from './_components/TipCard';
import { WorkingDots } from './_components/WorkingDots';
import { type ProgressStep } from './_hooks/useProgressSteps';
import * as styles from './LoadingScreen.css';

export type { ProgressStep };

interface Props {
messages: string[];
tips: string[];
warningMessage: string;
warningDelayMs: number;
progressSteps: ProgressStep[];
footerText: string;
onCancel: () => void;
}

export const LoadingScreen = ({
messages,
tips,
warningMessage,
warningDelayMs,
progressSteps,
footerText,
onCancel,
}: Props) => (
<div className={styles.container}>
<Mascot />
<WorkingDots />
<RotatingMessage messages={messages} warningMessage={warningMessage} warningDelayMs={warningDelayMs} />
<ProgressBar steps={progressSteps} />
<TipCard tips={tips} />
<LoadingFooter footerText={footerText} onCancel={onCancel} />
</div>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as styles from '../LoadingScreen.css';

interface Props {
footerText: string;
onCancel: () => void;
}

export const LoadingFooter = ({ footerText, onCancel }: Props) => (
<div className={styles.footer}>
<span className={styles.footerText}>{footerText}</span>
<button type="button" className={styles.cancelBtn} onClick={onCancel}>
취소
</button>
</div>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as styles from '../LoadingScreen.css';

export const Mascot = () => (
<div className={styles.mascotWrap} aria-hidden="true">
<div className={styles.mascotFloating}>
<svg className={styles.mascotImg} viewBox="0 0 318 225" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M223.42 0C275.175 0 317.132 41.8155 317.132 93.3975C317.132 93.4476 317.131 93.4977 317.131 93.5479V179.104C316.852 186.528 317.902 197.168 310.961 207.93C304.019 218.692 291.293 222.535 279.723 206.008C277.366 202.641 271.45 200.284 267.768 206.008C258.127 220.998 240.388 221.766 229.59 206.008C226.864 202.031 219.692 201.636 216.863 206.008C207.413 220.613 190.254 221.766 178.684 206.008C175.985 201.012 169.732 200.628 166.344 206.008C158.118 219.076 145.519 220.614 136.649 209.083C132.065 202.396 130.395 197.783 129.708 187.276V93.3936H129.709C129.711 41.8134 171.666 9.71069e-05 223.42 0Z"
fill="#8455DF"
/>
<path
d="M223.813 28.9412C261.903 28.9412 292.729 57.9991 292.729 93.781C292.729 129.563 261.903 158.621 223.813 158.621C185.723 158.621 154.897 129.563 154.897 93.781C154.897 57.9991 185.723 28.9412 223.813 28.9412Z"
fill="white"
stroke="#8455DF"
/>
<rect x="188.056" y="95.0508" width="16.7396" height="30.5168" rx="8.3698" fill="#8455DF" stroke="#8455DF" />
<rect x="238.188" y="95.0508" width="16.7396" height="30.5168" rx="8.3698" fill="#8455DF" stroke="#8455DF" />
<path
d="M10.5085 127.034C12.4797 119.917 20.1856 114.827 24.049 111.845C36.6964 102.603 64.7902 82.015 72.2542 76.5606C79.7183 71.1061 84.3583 74.3151 85.8508 76.5606C86.9066 77.7862 90.494 82.8818 98.4228 94.1442L99.2493 95.3182C109.884 110.424 97.1264 112.066 85.8508 114.394C79.5814 115.689 80.3527 121.838 83.8235 127.219L114.457 172.128C116.959 176.179 120.7 186.762 109.662 194.097C99.2495 201.015 89.3928 193.328 85.1405 186.88C79.6263 178.518 67.1428 159.426 61.3218 149.953C54.201 138.366 52.6641 136.564 44.1022 141.44C33.304 147.59 35.6751 160.593 29.1858 161.522C22.6965 162.451 20.3332 159.889 14.3422 150.446C8.3513 141.002 8.61658 133.866 10.5085 127.034Z"
fill="#8455DF"
stroke="#8455DF"
/>
</svg>
</div>
<div className={styles.mascotShadow} />
</div>
);
Loading
Loading