Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
f90bcd8
feat: ν™ˆν™”λ©΄ ν”Œλ‘œνŒ… css λ³€κ²½ (#60)
hansoojeongsj Oct 27, 2025
66e4a6a
feat: λΆ€λΆ„ 손보기 (#60)
hansoojeongsj Oct 27, 2025
a0929a4
feat: ν™ˆ ui κ°œμ„  (#60)
hansoojeongsj Oct 28, 2025
3ab9f1d
feat: μ„œλ²„ μ—°κ²°, μ—λŸ¬λ‘œλ”© κ΄€λ ¨ (#60)
hansoojeongsj Oct 28, 2025
c302132
feat: 전체 풀이 ν›„ μ™„λ£Œ λ©”μ‹œμ§€ 일단 주석 (#60)
hansoojeongsj Oct 28, 2025
e174401
feat: ν™ˆ ui (#60)
hansoojeongsj Oct 28, 2025
5fced04
feat: 전체 풀이 보고 μ™„λ£Œ μ±„νŒ… 및 λ§ˆμ΄νŽ˜μ΄μ§€ 칩리슀트 λ„ˆλΉ„ (#60)
hansoojeongsj Oct 28, 2025
a088c23
feat: solve server chat timeout μ£ΌκΈ° 및 λ§ˆμ΄νŽ˜μ΄μ§€ μΉ© 리슀트 λ„ˆλΉ„ μˆ˜μ • (#60)
hansoojeongsj Oct 28, 2025
ecd7413
feat: 헀더 λ³€κ²½ (#60)
hansoojeongsj Oct 28, 2025
e461088
feat: μ½”λ“œλž˜λΉ— μ‘°μ–Έ ν† κΈ€ μˆ˜μ • (#60)
hansoojeongsj Oct 28, 2025
c3481b2
feat: solve 이미지 두μž₯μΌλ•Œ me chat λ‘œλ”© μ±— λ‘κ°œ 및 μ‚¬μš©μž κ²½ν—˜ 더 μ’‹κ²Œ (#60)
hansoojeongsj Oct 28, 2025
43e07c5
feat: 둜그인 콜백 λ•Œ 헀더 μ—†μ• κΈ° (#60)
hansoojeongsj Oct 28, 2025
931f454
feat: μ½”λ“œλž˜λΉ— goback μˆ˜μ • (#60)
hansoojeongsj Oct 28, 2025
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
3 changes: 3 additions & 0 deletions public/svg/ic_left_arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/pages/home/components/scrollText/ScrollText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as styles from '@pages/home/components/scrollText/scrollText.css';

const ScrollText = () => {
const { scrollY } = useScroll();
const BREAKPOINT = 2480;
const BREAKPOINT = 2400;

const positionY = useTransform(scrollY, (y) =>
y < BREAKPOINT ? 'fixed' : 'absolute',
Expand Down
8 changes: 4 additions & 4 deletions src/pages/home/components/sectionBottom/SectionBottom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ const SectionBottom = () => {
}
}, []);

const FADE_IN_START = offsetTop - window.innerHeight / 2 + 200;
const FADE_IN_GAP = 200;
const FADE_OUT_GAP = 200;
const FADE_IN_START = offsetTop - window.innerHeight / 2;
const FADE_IN_GAP = 150;
const FADE_OUT_GAP = 150;

const opacityIn = useTransform(
scrollY,
Expand All @@ -33,7 +33,7 @@ const SectionBottom = () => {
const translateYIn = useTransform(
scrollY,
[FADE_IN_START, FADE_IN_START + FADE_IN_GAP],
[50, 0],
[-100, -150],
);

const opacityOut = useTransform(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { style } from '@vanilla-extract/css';
const sectionBottomWrapper = style({
position: 'relative',
width: '100%',
height: '88.8rem',
height: '93rem',
background: 'linear-gradient(180deg, #82acff 0%, #D7ECFF 85%)',
overflow: 'hidden',
});
Expand Down
17 changes: 10 additions & 7 deletions src/pages/home/components/sectionTop/SectionTop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ const SectionTop = () => {

return (
<div className={styles.sectionTopWrapper}>
{/* 전체 wrapper: translateY만 적용 */}
<motion.div
style={{
y: wrapperTranslateY,
Expand All @@ -80,17 +79,19 @@ const SectionTop = () => {
<motion.div
style={{
opacity: groupOpacity,
maxWidth: '768px',
maxWidth: 1180,
margin: '0 auto',
display: 'flex',
justifyContent: 'flex-end',
paddingRight: '3rem',
paddingRight: '3.6rem',
}}
>
<IcMainGroup
width={425.5}
height={426}
style={{ willChange: 'opacity' }}
style={{
willChange: 'opacity',
width: 'clamp(425.5px, 60vw, 600px)',
height: 'auto',
}}
/>
</motion.div>

Expand All @@ -107,7 +108,9 @@ const SectionTop = () => {
<IcMainChat1
width={168}
height={68}
style={{ willChange: 'opacity' }}
style={{
willChange: 'opacity',
}}
/>
</motion.div>

Expand Down
2 changes: 1 addition & 1 deletion src/pages/home/components/sectionTop/sectionTop.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export const sectionTopWrapper = style({
alignItems: 'center',

width: '100%',
height: '249.8rem',
height: '256rem',
background: 'linear-gradient(180deg, #2150EC 0%,#82acff 100%)',
});
18 changes: 13 additions & 5 deletions src/pages/home/home.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@ import { themeVars } from '@styles/theme.css';
import { style } from '@vanilla-extract/css';

export const floatingSolveBtn = style({
// 1. λ²„νŠΌμ„ 담을 'μ»¨ν…Œμ΄λ„ˆ'
display: 'flex',
position: 'fixed',
justifyContent: 'flex-end',

position: 'fixed',
bottom: '2rem',
zIndex: themeVars.zIndex.one,

// 2. μ»¨ν…Œμ΄λ„ˆλ₯Ό νŽ˜μ΄μ§€ '메인 컨텐츠'와 λ˜‘κ°™μ΄
width: '100%',
maxWidth: '1180px',

// 3. μ»¨ν…Œμ΄λ„ˆλ₯Ό ν™”λ©΄ 쀑앙에 배치
left: '50%',
transform: 'translateX(-50%)',

// 4. μ»¨ν…Œμ΄λ„ˆμ˜ 였λ₯Έμͺ½ μ•ˆμͺ½μ— 2rem 여백을 쀌
paddingRight: '2rem',

width: '100%',
maxWidth: '768px',
zIndex: themeVars.zIndex.one,
// padding이 λ„ˆλΉ„μ— 영ν–₯을 μ£Όμ§€ μ•Šλ„λ‘ μ„€μ •
boxSizing: 'border-box',
});
8 changes: 7 additions & 1 deletion src/pages/loginCallback/LoginCallback.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { routePath } from '@routes/routePath';
Expand All @@ -10,8 +10,14 @@ import { API_URL } from '@/shared/constants/apiURL';

const LoginCallback = () => {
const navigate = useNavigate();
const hasRunRef = useRef(false);

useEffect(() => {
if (hasRunRef.current) {
return;
}
hasRunRef.current = true;

const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const returnedState = params.get('state');
Expand Down
2 changes: 2 additions & 0 deletions src/pages/my/my.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ export const chipList = style({

export const chipListWrapper = style({
position: 'relative',
width: '100%',
maxWidth: '60rem',
});

export const chipGradientOverlay = style({
Expand Down
97 changes: 84 additions & 13 deletions src/pages/solve/Solve.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ import {
solutionStepsRef,
} from './ChatLogic';

const preloadImages = (urls: string[]): Promise<void> => {
const promises = urls.map((url) => {
return new Promise<void>((resolve, reject) => {
const img = new Image();
img.src = url;
img.onload = () => resolve();
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
});
});

return Promise.all(promises).then(() => undefined);
};

// νƒ€μž…
type StepItem = Record<`step ${number}`, string>;
type AnswerItem = { answer: string };
Expand All @@ -26,6 +39,8 @@ const Solve = () => {
const [downloadUrls, setDownloadUrls] = useState<string[]>([]);
const [s3Key, setS3Key] = useState('');
const [isPending, setIsPending] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [uploadingSlots, setUploadingSlots] = useState<number[]>([]);

const bottomRef = useRef<HTMLDivElement>(null);
const { mutateAsync: requestSolutionMutate } = usePostAiChat();
Expand All @@ -37,10 +52,11 @@ const Solve = () => {
requestAnimationFrame(() =>
bottomRef.current?.scrollIntoView({ behavior: 'smooth' }),
);
}, [chatList, isPending]);
}, [chatList, isPending, isUploading]);

const addChat = (chat: Chat) => setChatList((prev) => [...prev, chat]);
const addServerMessage = (text: string) => addChat({ from: 'server', text });

const handleImageSelect = (url: string) =>
addChat({ from: 'me', imageUrl: url });

Expand All @@ -53,7 +69,7 @@ const Solve = () => {

// ν† κΈ€ 클릭 ν•Έλ“€λŸ¬
const handleTextSelect = (text: string) => {
if (isPending) {
if (isPending || isUploading) {
return;
}
addChat({ from: 'me', text });
Expand All @@ -62,7 +78,12 @@ const Solve = () => {
text !== 'ν•΄κ²°ν–ˆμ–΄μš”!' &&
(!imageUploaded || !s3Key || !downloadUrls.length)
) {
return addServerMessage('문제 이미지λ₯Ό λ¨Όμ € μ—…λ‘œλ“œ ν•΄μ£Όμ„Έμš”!');
return setTimeout(() => {
addChat({
from: 'server',
text: '문제 이미지λ₯Ό λ¨Όμ € μ—…λ‘œλ“œ ν•΄μ£Όμ„Έμš”!',
});
}, 300);
}

switch (text) {
Expand Down Expand Up @@ -149,7 +170,12 @@ const Solve = () => {
};

const handleSolved = () => {
addChat({ from: 'server', text: '문제 해결을 μΆ•ν•˜ν•©λ‹ˆλ‹€!' });
setTimeout(() => {
addChat({
from: 'server',
text: '문제 해결을 μΆ•ν•˜ν•©λ‹ˆλ‹€!',
});
}, 300);
setTimeout(() => {
addChat({
from: 'server',
Expand Down Expand Up @@ -177,6 +203,13 @@ const Solve = () => {
text: solutionStepsRef.current.map((s) => s.text).join('\n\n'),
});

setTimeout(() => {
addChat({
from: 'server',
text: 'μƒˆλ‘œμš΄ 문제λ₯Ό μ§ˆλ¬Έν•˜λ €λ©΄ 카메라λ₯Ό λˆŒλŸ¬μ£Όμ„Έμš”.',
});
}, 1000);

// ν† κΈ€ κ·ΈλŒ€λ‘œ μœ μ§€
setToggleItems([
'단계별 풀이λ₯Ό μ•Œλ €μ€˜',
Expand All @@ -194,49 +227,74 @@ const Solve = () => {

// 파일 선택 ν•Έλ“€λŸ¬
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
// 1. 파일 선택 μ¦‰μ‹œ λͺ¨λ‹¬ λ‹«κΈ°
setIsOpen(false);

const files = e.target.files;
if (!files || files.length < expectedCount) {
addServerMessage(
expectedCount === 1
? '문제 이미지 1μž₯을 μ„ νƒν•΄μ£Όμ„Έμš”.'
: '문제 이미지 1μž₯, 풀이 이미지 1μž₯을 μ„ νƒν•΄μ£Όμ„Έμš”.',
);
e.target.value = '';
return;
}

// 2. 'me' λ‘œλ”© UI μ‹œμž‘ 및 μ΄ˆκΈ°ν™”
setUploadingSlots(Array.from({ length: expectedCount }, (_, i) => i));
setIsUploading(true);
const uploadedUrls: string[] = [];

try {
const {
uploadUrls,
downloadUrls: presignedUrls,
s3Key: presignedKey,
} = await getPresignedUrl(expectedCount);
const sortedFiles = Array.from(files).sort(
(a, b) => a.lastModified - b.lastModified,
);

// lastModified μ •λ ¬ 둜직 제거
const filesArray = Array.from(files);

// S3 μ—…λ‘œλ“œ 루프: filesArray의 μˆœμ„œλ₯Ό κ·ΈλŒ€λ‘œ μ‚¬μš©
// filesArray의 μˆœμ„œκ°€ κ³§ μ‚¬μš©μžκ°€ μ„ νƒν•œ μˆœμ„œ
for (let i = 0; i < expectedCount; i++) {
const response = await uploadToPresignedUrl(
uploadUrls[i],
sortedFiles[i]!,
filesArray[i]!,
);
if (!response.ok) {
throw new Error('S3 μ—…λ‘œλ“œ μ‹€νŒ¨');
}
handleImageSelect(presignedUrls[i]);
uploadedUrls.push(presignedUrls[i]);
}

// 3. S3 μ—…λ‘œλ“œ μ™„λ£Œ ν›„, λ‘œλ”©μ„ 끄지 μ•Šκ³  ν”„λ¦¬λ‘œλ”© μ‹œμž‘
await preloadImages(uploadedUrls);

// 4. ν”„λ¦¬λ‘œλ”© μ™„λ£Œ ν›„, λ‘œλ”© μ œκ±°ν•˜κ³  λ™μ‹œμ— 이미지 μΆ”κ°€
setUploadingSlots([]);
setIsUploading(false);

// uploadedUrlsλŠ” μ‚¬μš©μžκ°€ μ„ νƒν•œ μˆœμ„œλŒ€λ‘œ
uploadedUrls.forEach((url) => {
handleImageSelect(url);
});

setS3Key(presignedKey);
setDownloadUrls(presignedUrls);
setImageUploaded(true);
} catch {
// 5. μ‹€νŒ¨ μ‹œ
addServerMessage(
'이미지 μ—…λ‘œλ“œ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.',
);
// μ‹€νŒ¨ν•΄λ„ λ‘œλ”© μƒνƒœλŠ” μ΄ˆκΈ°ν™”
setUploadingSlots([]);
setIsUploading(false);
} finally {
// input μ΄ˆκΈ°ν™” (같은 파일 λ‹€μ‹œ 선택 κ°€λŠ₯ν•˜κ²Œ)
// 6. λͺ¨λ“  μž‘μ—…μ΄ λλ‚˜λ©΄ input μ΄ˆκΈ°ν™”
e.target.value = '';
// λͺ¨λ‹¬ λ‹«κΈ°
setIsOpen(false);
}
};

Expand Down Expand Up @@ -282,14 +340,27 @@ const Solve = () => {
</div>
)}

{isUploading &&
uploadingSlots.map((_, index) => (
<div key={index} className={styles.chatBubbleRight}>
<div className={styles.chatMyText}>
<div className={styles.dots}>
<span className={styles.dot} />
<span className={styles.dot} />
<span className={styles.dot} />
</div>
</div>
</div>
))}

<div ref={bottomRef} />
</div>

<Toggle
items={toggleItems}
onTextSelect={handleTextSelect}
onCameraClick={() => setIsOpen(true)}
disabled={isPending}
disabled={isPending || isUploading}
/>
<Modal
isOpen={isOpen}
Expand Down
10 changes: 10 additions & 0 deletions src/pages/solve/solve.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ const chatServerText = style({
...themeVars.font.bodySmall,
});

const chatMyText = style({
color: themeVars.color.point,
wordBreak: 'break-word',
...themeVars.font.bodySmall,
});

export {
wrapper,
chatContainer,
Expand All @@ -67,6 +73,7 @@ export {
chatImage,
chatText,
chatServerText,
chatMyText,
};

const bounce = keyframes({
Expand All @@ -87,6 +94,9 @@ export const dot = style({
backgroundColor: themeVars.color.point,
animation: `${bounce} 1.2s infinite ease-in-out both`,
selectors: {
[`${chatBubbleRight} &`]: {
backgroundColor: themeVars.color.white000,
},
'&:nth-child(1)': { animationDelay: '0s' },
'&:nth-child(2)': { animationDelay: '0.2s' },
'&:nth-child(3)': { animationDelay: '0.4s' },
Expand Down
Loading