-
Notifications
You must be signed in to change notification settings - Fork 1
[Refactor] - 통합 테스트 및 UI 개선 #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c2c87a7
a5a6b1e
14d31da
879a20d
ed96756
93facd1
24c88ff
7b4e488
30a51d2
d4a0bdf
df4e83d
6b9b2c1
d728ae6
4eab0b3
9b6d9ea
dfc99bc
ec875e1
7786912
c0dc13a
3e472ef
59d3f66
9f8da39
ec7bed1
173f826
64232db
3da21c8
47f3d66
2573882
c317d57
6f86a43
564945f
2a4aff4
81822e6
294bd70
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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' } }, | ||
|
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 }, | ||
| }, | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 메시지 회전과 팁 회전이 동일 패턴 두 번 반복 되고 있는 것 같은데, useRotatingIndex(count, interval, exit) 커스텀 훅으로 추출하면 좋을 것 같아요! 이런식으로 분리해서 로딩스크린 작성하면 좋을 것 같습니다! 😄 |
| 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> | ||
| ); |
Uh oh!
There was an error while loading. Please reload this page.