diff --git a/README.md b/README.md index 82b92ad..0843903 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,98 @@ -# ์„œ๋ก  +# ๐Ÿฅณ ๋ฐฐํฌ ๋งํฌ -์•ˆ๋…•ํ•˜์„ธ์š” ๐Ÿ™Œ๐Ÿป 19๊ธฐ ํ”„๋ก ํŠธ ์šด์˜์ง„ ๋ฐฐ์„ฑ์ค€์ž…๋‹ˆ๋‹ค. ์ด๋ฒˆ ๋ฏธ์…˜์—์„œ๋Š” ๋“œ๋””์–ด ํˆฌ๋‘๋ฆฌ์ŠคํŠธ์—์„œ ๋ฒ—์–ด๋‚˜ ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ์ธ **messenger** ๋งŒ๋“ค๊ธฐ๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. +https://react-messenger-19th-taupe.vercel.app/ -์ด๋ฒˆ์ฃผ๋Š” ํŠน๋ณ„ํžˆ **๋””์ž์ด๋„ˆ์™€์˜ ํ˜‘์—…**์œผ๋กœ ์ง„ํ–‰๋˜๋Š” ๋ฏธ์…˜์ž…๋‹ˆ๋‹ค. ๋””์ž์ด๋„ˆ๋ถ„๊ป˜์„œ ์—ด์‹ฌํžˆ ๋ฆฌ๋””์ž์ธ ํ•œ ๋ฉ”์‹ ์ € ํ”„๋กœ์ ํŠธ๋ฅผ ์—ฌ๋Ÿฌ๋ถ„๋“ค๊ป˜์„œ ๊ตฌํ˜„ํ•ด์ฃผ์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค. +# โš™๏ธ ๊ตฌํ˜„ ๊ธฐ๋Šฅ -๋™์‹œ์—, ์ด๋ฒˆ์ฃผ๋ถ€ํ„ฐ๋Š” ์ƒˆ๋กœ **TypeScript**๋ฅผ ์ ์šฉํ•ด๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. +image -ํ”„๋กœ์ ํŠธ์˜ ๊ทœ๋ชจ๊ฐ€ ์ปค์ง€๊ฒŒ ๋  ์ˆ˜๋ก, ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ฐ€์ง€๋Š” props์˜ ์ข…๋ฅ˜ ๋˜ํ•œ ๋‹ค์–‘ํ•ด์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋ฌด์ง€์„ฑ ์ฝ”๋”ฉ์„ ํ•˜๋‹ค๋ณด๋ฉด ๊ฐ€๋” ์ด props์˜ ๊ตฌ์„ฑ๊ณผ ์ด๋ฆ„, ์–ด๋–ค ํƒ€์ž…์ด ๋“ค์–ด๊ฐ€์•ผ ํ•˜๋Š”์ง€ ํ—ท๊ฐˆ๋ฆฌ๊ธฐ ๋งˆ๋ จ์ด์ฃ . ๋ณดํ†ต ๊ทธ๋Ÿด ๋•Œ ๋‹ค์‹œ ์ปดํฌ๋„ŒํŠธ ์ •์˜ ๋ถ€๋ถ„์œผ๋กœ ๋Œ์•„๊ฐ€ props์˜ ๊ตฌ์„ฑ์„ ๋ณด๊ณ  ์˜ค๊ณค ํ•ฉ๋‹ˆ๋‹ค. +![แ„’แ…ชแ„†แ…งแ†ซ แ„€แ…ตแ„…แ…ฉแ†จ 2024-03-29 แ„‹แ…ฉแ„’แ…ฎ 9 20 44 (1)](https://github.com/CEOS-Developers/react-messenger-19th/assets/101055312/f5128e75-1798-4afd-b183-713668a26bb6) -ํ•˜์ง€๋งŒ ์ด๋Ÿด ๋•Œ, typescript๋ฅผ ์ ์šฉํ•œ๋‹ค๋ฉด ์ปดํฌ๋„ŒํŠธ์˜ ๊ตฌ์„ฑ๊ณผ ๊ทธ ์ด๋ฆ„, ์‹ฌ์ง€์–ด ํƒ€์ž…๊นŒ์ง€ ํ•œ๋ฒˆ์— ์ž๋™์™„์„ฑ์œผ๋กœ ํŽธ๋ฆฌํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ณ , ์ƒ์‚ฐ์„ฑ์ด ์ฆ๋Œ€๋˜๊ฒ ์ฃ . +## React Hooks ์‚ฌ์šฉ -๋˜ํ•œ, **React Hooks**์— ์กฐ๊ธˆ ๋” ์ต์ˆ™ํ•ด์ง€๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ Hook๋“ค์ด ์žˆ์ง€๋งŒ ๊ทธ ์ค‘์—์„œ๋„ `useState`, `useEffect`, `useRef`๋ฅผ ์ค‘์ ์ ์œผ๋กœ ์‚ฌ์šฉํ•ด ๋ณด๋Š” ๋ฏธ์…˜์ธ๋ฐ์š”, React๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๊ต‰์žฅํžˆ ์ž์ฃผ ์“ฐ์ด๋Š” Hook๋“ค์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด ๋ถ€๋ถ„์„ ์ง‘์ค‘์ ์œผ๋กœ ๊ณต๋ถ€ํ•ด ๋ณด์•„์š”! +1. **useState**: ChatBottom ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๋ฉ”์‹œ์ง€ ๊ฐ’์„ ๊ด€๋ฆฌ ๋“ฑ ์ปดํฌ๋„ŒํŠธ์˜ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. +2. **useEffect**: UI์™€ ๊ด€๋ จ ์—†๋Š” ๋ถ€์ˆ˜ ํšจ๊ณผ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ์‹œ๋กœ, ChattingRoom ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋งˆ์šดํŠธ๋  ๋•Œ ์„œ๋ฒ„์—์„œ ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๊ฑฐ๋‚˜, ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก์ด ์—…๋ฐ์ดํŠธ๋  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ์Šคํฌ๋กค ์œ„์น˜๋ฅผ ์กฐ์ •ํ•  ๋•Œ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. +3. **useRef**: DOM ์š”์†Œ์— ์ง์ ‘ ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” ํ›…์ž…๋‹ˆ๋‹ค. ์ƒˆ ๋ฉ”์‹œ์ง€๊ฐ€ ์ถ”๊ฐ€๋  ๋•Œ๋งˆ๋‹ค ์ฑ„ํŒ… ๋ชฉ๋ก์˜ ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™์‹œํ‚ค๊ธฐ ์œ„ํ•ด messagesEndRef๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. -๊ทธ๋Ÿผ ์ด๋ฒˆ ๋ฏธ์…˜๋„ ํŒŒ์ดํŒ…์ž…๋‹ˆ๋‹ค!!๐ŸŽ‰ +- ์ž‘๋™ ๋ฐฉ์‹ : messagesEndRef๋Š” ์ฑ„ํŒ… ๋ชฉ๋ก ๋ ์ฐธ์กฐ โ†’ useEffect ํ›…์„ ํ†ตํ•ด, ๋ฉ”์‹œ์ง€๊ฐ€ ์ถ”๊ฐ€๋  ๋•Œ๋งˆ๋‹ค, current๊ฐ€ ๊ฐ€๋ฆฌํ‚ค๋Š” DOM ์š”์†Œ(์ฑ„ํŒ… ๋ชฉ๋ก ๋)๋กœ ์Šคํฌ๋กค์„ ์ด๋™์‹œํ‚ด -# ๋ฏธ์…˜ +```typescript +const messagesEndRef = useRef(null); -## Key Questions +//.. +useEffect(() => { + // ๋ฉ”์‹œ์ง€๊ฐ€ ์ถ”๊ฐ€๋  ๋•Œ๋งˆ๋‹ค ์Šคํฌ๋กค์„ ํ•˜๋‹จ์œผ๋กœ ์ด๋™ + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); +}, [messages]); +``` -- JavaScript๋ฅผ ์‚ฌ์šฉํ• ๋•Œ์— ๋น„ํ•ด TypeScript๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ์˜ ์žฅ์ ์€ ๋ฌด์—‡์ธ๊ฐ€์š”? -- ๋””์ž์ด๋„ˆ๋กœ๋ถ€ํ„ฐ ์ „๋‹ฌ๋ฐ›์€ ํ”ผ๊ทธ๋งˆ ๋งํฌ ํ˜น์€, ํ”ผ๊ทธ๋งˆ ์บก์ฒ˜๋ณธ -- ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ„๋ฆฌํ•œ ๊ธฐ์ค€์€ ๋ฌด์—‡์ธ๊ฐ€์š”? -- ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ์ ์šฉํ•˜๋ฉด์„œ ๋Š๋‚€ ์ ์€ ๋ฌด์—‡์ธ๊ฐ€์š”? -- ๋””์ž์ด๋„ˆ์™€ ์†Œํ†ตํ•˜๋ฉฐ ๋Š๋‚€์ ์€ ๋ฌด์—‡์ธ๊ฐ€์š”? +# โœ… Key Questions -## ๋ฏธ์…˜ ๋ชฉํ‘œ +## JavaScript๋ฅผ ์‚ฌ์šฉํ• ๋•Œ์— ๋น„ํ•ด TypeScript๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ์˜ ์žฅ์ ์€ ๋ฌด์—‡์ธ๊ฐ€์š”? -- TypeScript๋ฅผ ์‚ฌ์šฉํ•ด๋ด…์‹œ๋‹ค. -- useState๋กœ ์ปดํฌ๋„ŒํŠธ์˜ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. -- useEffect์™€ useRef์˜ ์‚ฌ์šฉ๋ฒ•์„ ์ดํ•ดํ•ฉ๋‹ˆ๋‹ค. -- styled-components๋ฅผ ํ†ตํ•œ CSS-in-JS ๋ฐ CSS Preprocessor์˜ ์‚ฌ์šฉ๋ฒ•์— ์ต์ˆ™ํ•ด์ง‘๋‹ˆ๋‹ค. +1. ํƒ€์ž… ์ฒดํฌ๋ฅผ ํ†ตํ•œ ์˜ค๋ฅ˜ ์˜ˆ๋ฐฉ -## ๊ธฐํ•œ +```typescript +interface Message { + id: number; + from: string; + content: string; + date: string; +} -2024๋…„ 3์›” 29์ผ ๊ธˆ์š”์ผ +interface ChatBodyProps { + messages: Message[]; + currentUser: string; + userImage: string; +} -## ํ•„์ˆ˜ ๊ตฌํ˜„ ๊ธฐ๋Šฅ +const [messages, setMessages] = useState([]); +``` -- ํ”ผ๊ทธ๋งˆ๋ฅผ ๋ณด๊ณ  [๊ฒฐ๊ณผํ™”๋ฉด](https://3th-fb-messenger.netlify.app)๊ณผ ๊ฐ™์ด ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. -- ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. -- ์ฑ„ํŒ…๋ฐฉ ์ƒ๋‹จ์˜ ํ”„๋กœํ•„์„ ํด๋ฆญํ•˜๋ฉด ์‚ฌ์šฉ์ž๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -- ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋‚ด๋ฉด ์ฑ„ํŒ…๋ฐฉ ํ•˜๋‹จ์œผ๋กœ ์Šคํฌ๋กค์„ ์ด๋™์‹œํ‚ต๋‹ˆ๋‹ค. -- ๋ฉ”์„ธ์ง€์— ์œ ์ € ์ •๋ณด(ํ”„๋กœํ•„ ์‚ฌ์ง„, ์ด๋ฆ„)๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. -- user์™€ message ๋ฐ์ดํ„ฐ๋ฅผ json ํŒŒ์ผ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. -- UI๋Š” **๋ฐ˜์‘ํ˜•์„ ์ œ์™ธ**ํ•˜๊ณ  ํ”ผ๊ทธ๋งˆํŒŒ์ผ์„ ๋”ฐ๋ผ์„œ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. +ChatBodyProps ์ธํ„ฐํŽ˜์ด์Šค๋Š” ChatBody ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฐ›์•„์•ผ ํ•  props์˜ ๊ตฌ์กฐ์™€ ํƒ€์ž…์„ ๋ช…์‹œ์ ์œผ๋กœ ์ •์˜ํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์—, +์ปดํฌ๋„ŒํŠธ์— ์ž˜๋ชป๋œ ํƒ€์ž…์˜ props๋ฅผ ์ „๋‹ฌํ•˜๊ฑฐ๋‚˜ ์ „๋‹ฌํ•˜์ง€ ์•Š์•˜์„ ๊ฒฝ์šฐ, ์ปดํŒŒ์ผ ๋‹จ๊ณ„์—์„œ ๋ฐ”๋กœ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. -### ์ถ”๊ฐ€ ๊ตฌํ˜„ ๊ธฐ๋Šฅ +๋˜ํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ํ•จ์ˆ˜ ๋‚ด์—์„œ newMessage ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•  ๋•Œ, Message ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ์š”๊ตฌํ•˜๋Š” ์†์„ฑ์„ ๋นผ๋จน๊ฑฐ๋‚˜ ์ž˜๋ชป๋œ ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜๋ฉด TypeScript ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ์ฆ‰์‹œ ์˜ค๋ฅ˜๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ๋–„๋ฌธ์—, ๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ , ์‹ค์ˆ˜ ๊ฐ€๋Šฅ์„ฑ์„ ์ค„์ž…๋‹ˆ๋‹ค. -- ๋”๋ธ” ํด๋ฆญ ํ•˜๋ฉด ๊ฐ์ •ํ‘œํ˜„์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. -- ๊ทธ ์™ธ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ์€ ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค๋ฉด ๋งˆ์Œ๊ป ์ถ”๊ฐ€ํ•ด ์ฃผ์„ธ์š”! +2. ์ฝ”๋“œ ์ž๋™ ์™„์„ฑ ๋ฐ ๋ฆฌํŒฉํ† ๋ง ์šฉ์ด + ์ปดํฌ๋„ŒํŠธ์— ์–ด๋–ค props๋ฅผ ์ „๋‹ฌํ•ด์•ผ ํ•˜๋Š”์ง€ ์‰ฝ๊ฒŒ ์•Œ ์ˆ˜ ์žˆ๊ณ , ์ด๋Š” ๊ฐœ๋ฐœ ํšจ์œจ์„ฑ์„ ๋†’์ž…๋‹ˆ๋‹ค. + ๋˜ํ•œ ํŠน์ • ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ตฌ์กฐ๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผ ํ•œ๋‹ค๋ฉด, TypeScript๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ•ด๋‹น ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ชจ๋“  ๊ณณ์„ ์ฐพ์•„ ์ˆ˜์ •ํ•ด์•ผ ํ•จ์„ ์•Œ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ๋”์šฑ ์•ˆ์ „ํ•˜๊ณ  ๋น ๋ฅธ ๋ฆฌํŒฉํ† ๋ง์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. -์ฐธ๊ณ ๋กœ ์ด๋ฒˆ ๊ณผ์ œ๋Š” ๋‹ค์Œ์ฃผ๊นŒ์ง€ ์ด์–ด์ง€๋Š” ๊ณผ์ œ์ด๋ฏ€๋กœ **ํ™•์žฅ์„ฑ**์„ ์ถฉ๋ถ„ํžˆ ๊ณ ๋ คํ•ด ์ฃผ์„ธ์š”. ์ฐธ๊ณ ๋กœ **4์ฃผ์ฐจ ๊ณผ์ œ์—์„œ๋Š” ์œ ์ € ๋ฐ ๊ธฐ๋Šฅ ์ถ”๊ฐ€์™€ Routing์„** ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด [recoil](https://recoiljs.org/ko/)์ด๋‚˜ [redux](https://ko.redux.js.org/introduction/getting-started/)๋ฅผ ์ด์šฉํ•œ ์ƒํƒœ๊ด€๋ฆฌ๋ฅผ ๋ฏธ๋ฆฌ ํ•ด๋ณด์‹œ๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค!! ๋ชจ๋‘ ๊ณต์‹๋ฌธ์„œ ๋งŽ์ด ์ฝ์–ด๋ณด์‹œ๊ณ  ์ž์‹ ๋งŒ์˜ ์ƒํƒœ๊ด€๋ฆฌ ์กฐํ•ฉ๋„ ์ฐพ์•„๋ณด๋ฉด ์žฌ๋ฐŒ์„ ๊ฑฐ์—์š” XD +3. ์ฝ”๋“œ ์˜๋„์™€ ๊ตฌ์กฐ ๋ช…ํ™•ํ™” + ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ์ฝ”๋“œ์˜ ์˜๋„์™€ ๊ตฌ์กฐ๋ฅผ ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๋ช…ํ™•ํžˆ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์–ด ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค. -## ๋งํฌ ๋ฐ ์ฐธ๊ณ ์ž๋ฃŒ +## ๋””์ž์ด๋„ˆ๋กœ๋ถ€ํ„ฐ ์ „๋‹ฌ๋ฐ›์€ ํ”ผ๊ทธ๋งˆ ๋งํฌ ํ˜น์€, ํ”ผ๊ทธ๋งˆ ์บก์ฒ˜๋ณธ -- [React docs - Hook](https://ko.reactjs.org/docs/hooks-intro.html) -- [React์˜ Hooks ์™„๋ฒฝ ์ •๋ณตํ•˜๊ธฐ](https://velog.io/@velopert/react-hooks#1-usestate) -- [useEffect ์™„๋ฒฝ ๊ฐ€์ด๋“œ](https://overreacted.io/ko/a-complete-guide-to-useeffect/) -- [์ฝ”๋”ฉ ์ปจ๋ฒค์…˜](https://ui.toast.com/fe-guide/ko_CODING-CONVENTION) -- [ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ํ•ธ๋“œ๋ถ](https://joshua1988.github.io/ts/intro.html) -- [๋ฆฌ์•กํŠธ ํ”„๋กœ์ ํŠธ์—์„œ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์‚ฌ์šฉํ•˜๊ธฐ (์‹œ๋ฆฌ์ฆˆ)](https://velog.io/@velopert/series/react-with-typescript) -- [๋””์ž์ธ ์‹œ์Šคํ…œ ๊ตฌ์ถ•๊ธฐ](https://yozm.wishket.com/magazine/detail/1830/) -- [[์˜์ƒ] : ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•œ ์ดํ•ด](https://www.youtube.com/watch?v=21eiJc90ggo) -- [Styled Component๋กœ ๋””์ž์ธ ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ](https://zaat.dev/blog/building-a-design-system-in-react-with-styled-components/) -- [ts ์ ˆ๋Œ€๊ฒฝ๋กœ ์„ค์ •ํ•˜๊ธฐ](https://tesseractjh.tistory.com/232) +แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2024-03-29 แ„‹แ…ฉแ„’แ…ฎ 7 16 01 + +## ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ„๋ฆฌํ•œ ๊ธฐ์ค€์€ ๋ฌด์—‡์ธ๊ฐ€์š”? + +1. ๊ฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ UI ์˜์—ญ๋ณ„ & ๊ธฐ๋Šฅ๋ณ„๋กœ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. + ๋‹จ์ผ ์ฑ…์ž„ ์›์น™์— ๋”ฐ๋ผ ํ•œ ๊ฐ€์ง€ ๊ธฐ๋Šฅ๋งŒ ์ˆ˜ํ–‰ํ•˜๋„๋ก ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค. + +- **ChatHead**: ์ฑ„ํŒ…๋ฐฉ์˜ ์ƒ๋‹จ์— ์œ„์น˜ํ•œ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์ด๋ฆ„๊ณผ ์ด๋ฏธ์ง€๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. +- **ChatBody**: ์ฑ„ํŒ…๋ฐฉ์—์„œ์˜ ๋Œ€ํ™” ๋‚ด์šฉ์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”์‹œ์ง€๋งˆ๋‹ค ์‹œ๊ฐ„๊ณผ ๋‚ ์งœ, ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. +- **ChatBottom**: ๋ฉ”์‹œ์ง€ ์ž…๋ ฅ์ฐฝ๊ณผ ์ „์†ก ๋ฒ„ํŠผ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ๋ฐ›๊ณ , 'Enter' ํ‚ค๋‚˜ ์ „์†ก ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. + +2. ๋˜ํ•œ, ๋ฒ„ํŠผ, ์ž…๋ ฅ ํ•„๋“œ๊ฐ™์ด, ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ์— ๋”ฐ๋ผ, ํŠน์ • ๊ธฐ๋Šฅ์„ ํ•˜๊ณ , ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” UI๋“ค๋„ ์žฌ์‚ฌ์šฉ์ด ์šฉ์ดํ•˜๊ฒŒ ๋ณ„๋„์˜ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. + +## ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ์ ์šฉํ•˜๋ฉด์„œ ๋Š๋‚€ ์ ์€ ๋ฌด์—‡์ธ๊ฐ€์š”? + +๋””์ž์ธ ์‹œ์Šคํ…œ์„ ์ ์šฉํ•จ์œผ๋กœ์จ UI์˜ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์—ˆ๋˜ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. +๋˜ํ•œ, ๊ฐœ๋ฐœ ๊ณผ์ •์—์„œ๋„ ํ™•์‹คํžˆ ํŽธ๋ฆฌํ•จ์„ ๋Š๊ผˆ์Šต๋‹ˆ๋‹ค. +์šฐ์„ , ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ํ†ตํ•ด ๊ฐœ๋ฐœ์ž์™€ ๋””์ž์ด๋„ˆ ๊ฐ„์˜ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜์ด ๋ช…ํ™•ํ•ด์กŒ๊ณ , ์ผ๊ด€๋œ ๋””์ž์ธ ์–ธ์–ด๋ฅผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. +๋˜ํ•œ, UX/UI์ ์ธ ๊ณ ๋ฏผ์„ ๋œ์–ด ๊ฐœ๋ฐœ์—๋งŒ ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ์–ด ์ข‹์•˜๋˜ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. +๋””์ž์ธ ์‹œ์Šคํ…œ์„ ํ™œ์šฉํ•˜๋ฉด์„œ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค ๋•Œ๋Š” ์ •๋ง ์—ฌ๋Ÿฌ UI ์š”์†Œ๋“ค์ด ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ๋Š๊ผˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ UI ์š”์†Œ๋“ค์€ ๋ชจ๋‘ ๊ต์ฒด ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์œผ๋ฏ€๋กœ, ์„œ๋น„์Šค์˜ ๊ทœ๋ชจ๊ฐ€ ์ปค์งˆ์ˆ˜๋ก, ๋‚˜์ค‘์— UI ์š”์†Œ๋“ค์„ ๊ต์ฒดํ•˜๊ธฐ ํŽธํ•˜๊ฒŒ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ  ๊ฐ€๋…์„ฑ ์žˆ๋Š” ์ฝ”๋“œ๋ฅผ ์งœ๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค๋Š” ๊นจ๋‹ฌ์Œ์„ ์–ป์—ˆ์Šต๋‹ˆ๋‹ค. + +## ๋””์ž์ด๋„ˆ์™€ ์†Œํ†ตํ•˜๋ฉฐ ๋Š๋‚€์ ์€ ๋ฌด์—‡์ธ๊ฐ€์š”? + +ํ”ผ๊ทธ๋งˆ์— ๋งŒ๋“ค์–ด์ฃผ์‹  UI๋ฅผ ๊ฐ€์ง€๊ณ  ๊ฐœ๋ฐœํ•˜๋ฉด์„œ, ํ˜ผ์ž ๊ฐœ๋ฐœํ•˜๊ณ , UI๊นŒ์ง€ ๋งŒ๋“ค ๋•Œ์™€๋Š” ํ€„๋ฆฌํ‹ฐ๊ฐ€ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๋ฌผ์ด ๋‚˜์˜จ ๊ฒƒ ๊ฐ™๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ญ๋‹ˆ๋‹ค. +ํŠนํžˆ, ๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•  ๋•Œ๋Š” ์•„์ด์ฝ˜์ด ๋ณด๋‚ด๊ธฐ ๋ฒ„ํŠผ์ด๊ณ , ์ž…๋ ฅํ•˜์ง€ ์•Š์„ ๋•Œ๋Š” ๋งˆ์ดํฌ ์•„์ด์ฝ˜์ธ ๊ฒƒ์€, ๋„ˆ๋ฌด ๋‹น์—ฐํ•œ UX์ด๋‚˜, ์ผ์ƒ ์†์—์„œ๋Š” ์‹ ๊ฒฝ์“ฐ์ง€ ์•Š๊ณ  ์ง€๋‚˜์ณค์—ˆ๋Š”๋ฐ, ํ”ผ๊ทธ๋งˆ์—์„œ UI ํ•˜๋‚˜ํ•˜๋‚˜๋ฅผ ๊ฐ€์ ธ๋‹ค ๊ฐœ๋ฐœํ•˜๋ฉด์„œ, ์ •๋ง ์ด๋Š” ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ๊ฒƒ ๊ฐ™๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. + +# ๐Ÿ’ก ๋Š๋‚€ ์  + +์ฒ˜์Œ์—๋Š” json์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•œ๋‹ค๋Š”๊ฒŒ, ์ƒˆ๋กœ ์ž…๋ ฅ๋œ ๋Œ€ํ™” ๋‚ด์—ญ๋„ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ด, ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ• ์ง€ ๊ฐ์ด ์žกํžˆ์ง€ ์•Š์•„, +์šฐ์„ ์€ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์™€ useEffect๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋Œ€ํ™” ๋‚ด์—ญ์„ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. +json์— ๋”๋ฏธ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ด์•ผํ•˜๋Š” ๊ฒƒ์„ ์ด์–ด์„œ ๊ณ„์† ๊ณ ๋ฏผํ•ด๋ณด๋‹ค๊ฐ€ ๋ฐฑ์—”๋“œ๋„ ๊ตฌํ˜„ํ•ด์•ผ ํ•˜๋Š”๊ฑด๊ฐ€?...๋ผ๋Š” ์ƒ๊ฐ์„ ํ•˜๋ฉฐ json-server๋ฅผ ํ†ตํ•ด json ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฒ•์„ ๊ณ„์†ํ•ด์„œ ์‹œ๋„ํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค. +๊ทธ๋Ÿฐ๋ฐ, ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋งŒ json ํŒŒ์ผ์— ์ €์žฅํ•˜๊ณ , ์ด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ฒƒ์ด๋ž€ ๊ฑธ ์•Œ๊ณ , ์•ˆ์‹ฌํ•˜๋ฉฐ ์ดˆ๊ธฐ ๋ฉ”์‹œ์ง€ ๋กœ๋“œ ํ•˜๋Š” ๋ถ€๋ถ„์„ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ, -> json ํŒŒ์ผ์—์„œ json ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์œผ๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. +์ด๋ฅผ ํ†ตํ•ด, json์— ์ƒˆ๋กœ ์ž…๋ ฅ๋œ ๋Œ€ํ™” ๋‚ด์—ญ๋„ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ๋„ ๊ตฌํ˜„ํ•ด๋ณด๊ณ  ์‹ถ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋„ˆ๋ฌด ์ปค์ ธ์„œ, ์ค‘๊ฐ„๊ณ ์‚ฌ ์ดํ›„์— ์ด ๋ถ€๋ถ„์„ ๊ตฌํ˜„ํ•˜๋Š”๊ฒŒ ๊ธฐ๋Œ€๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. diff --git a/image.png b/image.png new file mode 100644 index 0000000..b085d62 Binary files /dev/null and b/image.png differ diff --git a/package-lock.json b/package-lock.json index c27bbe4..4769908 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,13 @@ "@types/node": "^16.18.91", "@types/react": "^18.2.69", "@types/react-dom": "^18.2.22", + "nodejs": "^0.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.23.0", "react-scripts": "5.0.1", + "recoil": "^0.7.7", + "styled-components": "^6.1.8", "typescript": "^4.9.5", "web-vitals": "^2.1.4" } @@ -2288,6 +2292,24 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -3338,6 +3360,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.0.tgz", + "integrity": "sha512-Quz1KOffeEf/zwkCBM3kBtH4ZoZ+pT3xIXBG4PPW/XFtDP7EGhtTiC2+gpL9GnR7+Qdet5Oa6cYSvwKYg6kN9Q==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4290,6 +4320,11 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" }, + "node_modules/@types/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" + }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.9", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", @@ -5750,6 +5785,14 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -6182,6 +6225,14 @@ "postcss": "^8.4" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, "node_modules/css-declaration-sorter": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", @@ -6372,6 +6423,16 @@ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -8861,6 +8922,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==" + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -12623,6 +12689,11 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, + "node_modules/nodejs": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/nodejs/-/nodejs-0.0.0.tgz", + "integrity": "sha512-1V+0HwaB/dhxzidEFc4uJ3k52gLI4B6YBZgJIofjwYCSAkD6CI0me6TDBT2QM2nbGWNxCHcq9/wVynzQYZOhUg==" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -14864,6 +14935,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.0.tgz", + "integrity": "sha512-wPMZ8S2TuPadH0sF5irFGjkNLIcRvOSaEe7v+JER8508dyJumm6XZB1u5kztlX0RVq6AzRVndzqcUh6sFIauzA==", + "dependencies": { + "@remix-run/router": "1.16.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.0.tgz", + "integrity": "sha512-Q9YaSYvubwgbal2c9DJKfx6hTNoBp3iJDsl+Duva/DwxoJH+OTXkxGpql4iUK2sla/8z4RpjAm6EWx1qUDuopQ==", + "dependencies": { + "@remix-run/router": "1.16.0", + "react-router": "6.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -14968,6 +15069,25 @@ "node": ">=8.10.0" } }, + "node_modules/recoil": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", + "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", + "dependencies": { + "hamt_plus": "1.0.2" + }, + "peerDependencies": { + "react": ">=16.13.1" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -15719,6 +15839,11 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -16260,6 +16385,70 @@ "webpack": "^5.0.0" } }, + "node_modules/styled-components": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.8.tgz", + "integrity": "sha512-PQ6Dn+QxlWyEGCKDS71NGsXoVLKfE1c3vApkvDYS5KAK+V8fNWGhbSUEo9Gg2iaID2tjLXegEW3bZDUGpofRWw==", + "dependencies": { + "@emotion/is-prop-valid": "1.2.1", + "@emotion/unitless": "0.8.0", + "@types/stylis": "4.2.0", + "css-to-react-native": "3.2.0", + "csstype": "3.1.2", + "postcss": "8.4.31", + "shallowequal": "1.1.0", + "stylis": "4.3.1", + "tslib": "2.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -16275,6 +16464,11 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", + "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", diff --git a/package.json b/package.json index ea335d3..1ee9965 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,13 @@ "@types/node": "^16.18.91", "@types/react": "^18.2.69", "@types/react-dom": "^18.2.22", + "nodejs": "^0.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.23.0", "react-scripts": "5.0.1", + "recoil": "^0.7.7", + "styled-components": "^6.1.8", "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, diff --git a/public/github_logo.png b/public/github_logo.png new file mode 100644 index 0000000..d97dc61 Binary files /dev/null and b/public/github_logo.png differ diff --git a/public/instagram_logo.png b/public/instagram_logo.png new file mode 100644 index 0000000..611e5af Binary files /dev/null and b/public/instagram_logo.png differ diff --git a/public/item/back.png b/public/item/back.png new file mode 100644 index 0000000..b15dad2 Binary files /dev/null and b/public/item/back.png differ diff --git a/public/item/chatBottom.png b/public/item/chatBottom.png new file mode 100644 index 0000000..402dc13 Binary files /dev/null and b/public/item/chatBottom.png differ diff --git a/public/item/chatFalse.png b/public/item/chatFalse.png new file mode 100644 index 0000000..957f544 Binary files /dev/null and b/public/item/chatFalse.png differ diff --git a/public/item/chatHead.png b/public/item/chatHead.png new file mode 100644 index 0000000..a72c9eb Binary files /dev/null and b/public/item/chatHead.png differ diff --git a/public/item/chatTrue.png b/public/item/chatTrue.png new file mode 100644 index 0000000..cedeae8 Binary files /dev/null and b/public/item/chatTrue.png differ diff --git a/public/item/micIcon.png b/public/item/micIcon.png new file mode 100644 index 0000000..d58f6ed Binary files /dev/null and b/public/item/micIcon.png differ diff --git a/public/item/profileFalse.png b/public/item/profileFalse.png new file mode 100644 index 0000000..f3fa692 Binary files /dev/null and b/public/item/profileFalse.png differ diff --git a/public/item/profileTrue.png b/public/item/profileTrue.png new file mode 100644 index 0000000..21786f9 Binary files /dev/null and b/public/item/profileTrue.png differ diff --git a/public/item/profile_mini.png b/public/item/profile_mini.png new file mode 100644 index 0000000..0942828 Binary files /dev/null and b/public/item/profile_mini.png differ diff --git a/public/item/profile_mini2.png b/public/item/profile_mini2.png new file mode 100644 index 0000000..c284156 Binary files /dev/null and b/public/item/profile_mini2.png differ diff --git a/public/item/searchbar.png b/public/item/searchbar.png new file mode 100644 index 0000000..64f91ee Binary files /dev/null and b/public/item/searchbar.png differ diff --git a/public/item/sendIcon.png b/public/item/sendIcon.png new file mode 100644 index 0000000..05bec3e Binary files /dev/null and b/public/item/sendIcon.png differ diff --git a/public/item/userFalse.png b/public/item/userFalse.png new file mode 100644 index 0000000..699ce49 Binary files /dev/null and b/public/item/userFalse.png differ diff --git a/public/item/userTrue.png b/public/item/userTrue.png new file mode 100644 index 0000000..e58b9a6 Binary files /dev/null and b/public/item/userTrue.png differ diff --git a/public/item/userbg.png b/public/item/userbg.png new file mode 100644 index 0000000..8572ea4 Binary files /dev/null and b/public/item/userbg.png differ diff --git a/src/App.css b/src/App.css index 74b5e05..e69de29 100644 --- a/src/App.css +++ b/src/App.css @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/App.tsx b/src/App.tsx index bd79c18..aa1ce6c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,46 @@ -function App() { +import React from 'react'; +import { Route, Routes, useLocation } from 'react-router-dom'; +import ChattingRoom from './pages/ChattingRoom'; +import UserList from './pages/userList'; +import ChattingList from './pages/ChattingList'; +import Profile from './pages/Profile'; +import Navbar from './components/Navbar'; +import NavbarHead from './components/NavbarHead'; +import ChatHead from './components/ChatHead'; +import ChatBody from './components/ChatBody'; +import ChatBottom from './components/ChatBottom'; +import GlobalStyle from './style/GlobalStyle'; +import styled from 'styled-components'; + + + +const App = () => { + const { pathname } = useLocation(); + const isChattingRoomPage = pathname.startsWith('/chattingroom'); + return ( -
-

19๊ธฐ ํ”„๋ก ํŠธ์—”๋“œ ํŒŒ์ดํŒ…!!! ๋””์ž์ธ๊ณผ ์‚ฌ์ด์ข‹๊ฒŒ ์ง€๋‚ด์š”~~~

-
+ <> + + {isChattingRoomPage ? ( + + ) : ( + // + + )} + +
+ + } /> + } /> + } /> + } /> + +
+ {!isChattingRoomPage && } + + + ); -} +}; export default App; diff --git a/src/ChattingRoom.js b/src/ChattingRoom.js new file mode 100644 index 0000000..b0af876 --- /dev/null +++ b/src/ChattingRoom.js @@ -0,0 +1,87 @@ +/*import React, { useState, useEffect, useRef } from 'react'; +import ChatHead from './components/ChatHead'; +import ChatBody from './components/ChatBody'; +import ChatBottom from './components/ChatBottom'; +import dummy from './dummy.json'; + +interface Message { + id: number; + from: string; + content: string; + date: string; +} + +interface User { + name: string; + image: string; +} + +const initialUsers: User[] = [ + { name: '์ด์ง€์ธ', image: '/item/profile_mini.png' }, + { name: '์ด์˜์ธ', image: '/item/profile_mini2.png' }, +]; + +const initialMessages: Message[] = []; + +function ChattingRoom() { + // ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ƒํƒœ ๊ด€๋ฆฌ + const [currentUserIndex, setCurrentUserIndex] = useState(0); + // ํ˜„์žฌ ๋ฉ”์‹œ์ง€ ๊ธฐ๋กฅ ์ƒํƒœ ๊ด€๋ฆฌ + const [messages, setMessages] = useState(() => { + // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์ดˆ๊ธฐ ๋ฉ”์‹œ์ง€ ๋กœ๋“œ + // const savedMessages = localStorage.getItem('messages'); + const savedMessages = dummy; + //return savedMessages ? JSON.parse(savedMessages) : []; + return savedMessages; + }); + const currentUser = initialUsers[currentUserIndex]; + const otherUserIndex = (currentUserIndex + 1) % initialUsers.length; + const otherUser = initialUsers[otherUserIndex]; + + // ๋ฉ”์„ธ์ง€ ๋ชฉ๋ก ๋์œผ๋กœ ์Šคํฌ๋กค + const messagesEndRef = useRef(null); + + useEffect(() => { + // ๋ฉ”์‹œ์ง€ ๋ฐ์ดํ„ฐ๋ฅผ localStorage์— ์ €์žฅ + localStorage.setItem('messages', JSON.stringify(messages)); + + //dummy.message = JSON.stringify(messages); + //console.log(dummy.message); + }, [messages]); + + useEffect(() => { + // ๋ฉ”์‹œ์ง€๊ฐ€ ์ถ”๊ฐ€๋  ๋•Œ๋งˆ๋‹ค ์Šคํฌ๋กค์„ ํ•˜๋‹จ์œผ๋กœ ์ด๋™ + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + const sendMessage = (messageContent: string) => { + const newMessage: Message = { + id: Date.now(), + from: currentUser.name, + content: messageContent, + date: new Date().toISOString(), // ISO ํ˜•์‹์˜ ๋‚ ์งœ ๋ฌธ์ž์—ด ์‚ฌ์šฉ + }; + + setMessages((prevMessages) => [...prevMessages, newMessage]); + }; + + const toggleUser = () => { + setCurrentUserIndex((currentIndex) => (currentIndex === 0 ? 1 : 0)); + }; + + return ( +
+ + + +
+
+ ); +} + +export default ChattingRoom; +*/ diff --git a/src/components/ChatBody.tsx b/src/components/ChatBody.tsx new file mode 100644 index 0000000..758b262 --- /dev/null +++ b/src/components/ChatBody.tsx @@ -0,0 +1,81 @@ + +import React from 'react'; +import { + ChatBodyContainer, + TimeDisplay, + DateSeparator, + ChatMessageBox, + UserProfileImage + +} from '../style/ChatBodyStyle'; + +interface Message { + id: number; + from: string; + content: string; + date: string; +} + +interface ChatBodyProps { + messages: Message[]; + currentUser: string; + userImage: string; +} + +const formatDate = (isoDateString: string) => { + const date = new Date(isoDateString); + return `${date.getFullYear()}๋…„ ${date.getMonth() + 1}์›” ${date.getDate()}์ผ`; +}; + +const formatTime = (isoDateString: string) => { + const date = new Date(isoDateString); + return `${date.getHours().toString().padStart(2, '0')}:${date + .getMinutes() + .toString() + .padStart(2, '0')}`; +}; + +const ChatBody: React.FC = ({ messages,userImage, currentUser}) => { + let lastDate = ''; + let lastMinute = ''; + + + return ( + + {messages.map((message, index) => { + const messageDate = formatDate(message.date); + const messageMinute = formatTime(message.date); + let displayDateSeparator = false; + let displayMinuteSeparator = false; + + if (messageDate !== lastDate) { + lastDate = messageDate; + displayDateSeparator = true; + } + + if (messageMinute !== lastMinute) { + lastMinute = messageMinute; + displayMinuteSeparator = true; + } + + return ( + + {displayDateSeparator && ( + {formatDate(message.date)} + )} + {displayMinuteSeparator && ( + {formatTime(message.date)} + )} + + +
{message.content}
+
+ +
+ ); + })} +
+ ); +}; + +export default ChatBody; \ No newline at end of file diff --git a/src/components/ChatBottom.tsx b/src/components/ChatBottom.tsx new file mode 100644 index 0000000..1196a9f --- /dev/null +++ b/src/components/ChatBottom.tsx @@ -0,0 +1,51 @@ +import React, { useState, KeyboardEvent, ChangeEvent } from 'react'; + +import { + ChatBottomContainer, + ChatInput, + SendButton, +} from '../style/ChatBottomStyle'; + +const sendIcon = '/item/sendIcon.png'; +const micIcon = '/item/micIcon.png'; + +interface ChatBottomProps { + onSendMessage: (messageContent: string) => void; +} + +const ChatBottom: React.FC = ({ onSendMessage }) => { + const [inputValue, setInputValue] = useState(''); + + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key === 'Enter' && inputValue.trim()) { + sendMessage(); + } + }; + + const sendMessage = () => { + if (inputValue.trim()) { + onSendMessage(inputValue.trim()); + setInputValue(''); + } + }; + + // ์ž…๋ ฅ๋œ ํ…์ŠคํŠธ์— ๋”ฐ๋ผ ์ด๋ฏธ์ง€ ๊ฒฐ์ • + const buttonImage = inputValue.trim() ? sendIcon : micIcon; + + return ( + + ) => + setInputValue(e.target.value) + } + onKeyPress={handleKeyPress} + /> + + + ); +}; + +export default ChatBottom; diff --git a/src/components/ChatHead.tsx b/src/components/ChatHead.tsx new file mode 100644 index 0000000..65a9ce1 --- /dev/null +++ b/src/components/ChatHead.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { + ChatHeadContainer, + UserImage, + UserName, + SendButton, +} from '../style/ChatHeadStyle'; +import { useNavigate } from 'react-router-dom'; + +interface User { + name: string; + image: string; +} + +interface ChatHeadProps { + user: User; + onUserClick: () => void; +} + +const buttonImage = '/item/back.png'; + +const ChatHead: React.FC = ({ user, onUserClick }) => { + const navigate = useNavigate(); + + const handleUserClick = () => { + navigate(`/chattinglist`); + }; + + return ( + + + + {user.name} + + ); +}; + +export default ChatHead; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..2401871 --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,81 @@ +// src/components/Navbar.tsx + +import React from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import styled from 'styled-components'; + +const NavbarContainer = styled.div` + display: flex; + justify-content: space-around; + position: fixed; + font-size: 20px; + width: 375px; + bottom: 300px; + height: 50px; + background: #f0f0f0; + padding-top: 30px; +`; + +const NavbarLink = styled(Link)` + text-decoration: none; + color: black; +`; + +const NavButton = styled.img` + position: fixed; + bottom: 320px; + margin-left: -35px; + /* width: 30px; +height: 30px; +margin-right: 10px; */ +`; + +const Navbar = () => { + const { pathname } = useLocation(); + let userPageButton; + let chatPageButton; + let profilePageButton; + + switch (pathname) { + case '/chattingroom': + userPageButton = '/item/back.png'; + chatPageButton = '/item/back.png'; + profilePageButton = '/item/profileFalse.png'; + break; + case '/userlist': + userPageButton = '/item/userTrue.png'; + chatPageButton = '/item/chatFalse.png'; + profilePageButton = '/item/profileFalse.png'; + break; + case '/chattinglist': + userPageButton = '/item/userFalse.png'; + chatPageButton = '/item/chatTrue.png'; + profilePageButton = '/item/profileFalse.png'; + break; + case '/profile': + userPageButton = '/item/userFalse.png'; + chatPageButton = '/item/chatFalse.png'; + profilePageButton = '/item/profileTrue.png'; + break; + default: + userPageButton = '/item/userFalse.png'; + chatPageButton = '/item/chatFalse.png'; + profilePageButton = '/item/profileFalse.png'; + } + + return ( + + + + + + + + + + + + ); +}; + +export default Navbar; diff --git a/src/components/NavbarHead.tsx b/src/components/NavbarHead.tsx new file mode 100644 index 0000000..653d5a8 --- /dev/null +++ b/src/components/NavbarHead.tsx @@ -0,0 +1,57 @@ +// src/components/NavbarHead.tsx + +import React from 'react'; +import { useLocation } from 'react-router-dom'; +import styled from 'styled-components'; + +const NavbarHeadContainer = styled.div` + display: flex; + justify-content: flex-start; + position: fixed; + font-size: 20px; + padding-left: 15px; + width: 375px; + top: -33px; + height: 80px; + background: #fffff; + padding-top: 30px; + + +`; +const NavbarHeadTexts=styled.div` +background: #fffff; +padding-top: 30px; +font-family: 'Noto Sans KR'; +font-size: 20px; +font-style: normal; +font-weight: 500; +`; + +const NavbarHead = () => { + const { pathname } = useLocation(); + let navText; + + switch (pathname) { + case '/chattingroom': + navText = ''; + break; + case '/userlist': + navText = '์นœ๊ตฌ ๋ชฉ๋ก'; + break; + case '/chattinglist': + navText = '๋Œ€ํ™”'; + break; + case '/profile': + navText = '๋‚ด ํ”„๋กœํ•„'; + break; + default: + navText = ''; + } + + return( + + {navText} + ); +}; + +export default NavbarHead; diff --git a/src/dummy.json b/src/dummy.json new file mode 100644 index 0000000..782935e --- /dev/null +++ b/src/dummy.json @@ -0,0 +1,35 @@ + + +[ + { + "id": 1711705694689, + "from": "์ด์ง€์ธ", + "content": "๊ตญ๊ฐ€๋Š” ๋†์ˆ˜์‚ฐ๋ฌผ์˜ ์ˆ˜๊ธ‰๊ท ํ˜•๊ณผ ์œ ํ†ต๊ตฌ์กฐ์˜ ๊ฐœ์„ ์— ๋…ธ๋ ฅํ•˜์—ฌ", + "date": "2024-03-29T09:48:14.689Z" + }, + { + "id": 1711705702873, + "from": "์ด์˜์ธ", + "content": "๊ฐ€๊ฒฉ์•ˆ์ •์„ ๋„๋ชจํ•จ์œผ๋กœ์จ ๋†ยท์–ด๋ฏผ์˜ ์ด์ต์„ ๋ณดํ˜ธํ•œ๋‹ค.", + "date": "2024-03-29T09:48:22.873Z" + }, + { + "id": 1711705711651, + "from": "์ด์ง€์ธ", + "content": "์ •๋‹น์€ ๊ทธ ๋ชฉ์ ยท์กฐ์ง๊ณผ ํ™œ๋™์ด ๋ฏผ์ฃผ์ ์ด์–ด์•ผ ํ•˜๋ฉฐ,", + "date": "2024-03-29T09:48:31.651Z" + }, + { + "id": 1711705718508, + "from": "์ด์ง€์ธ", + "content": "๊ตญ๋ฏผ์˜ ์ •์น˜์  ์˜์‚ฌํ˜•์„ฑ์— ์ฐธ์—ฌ", + "date": "2024-03-29T09:48:38.508Z" + }, + { + "id": 1711705726747, + "from": "์ด์˜์ธ", + "content": "ํ•˜๋Š”๋ฐ ํ•„์š”ํ•œ ์กฐ์ง์„ ๊ฐ€์ ธ์•ผ ํ•œ๋‹ค.", + "date": "2024-03-29T09:48:46.747Z" + } + +] \ No newline at end of file diff --git a/src/index.css b/src/index.css index ec2585e..e69de29 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/src/index.tsx b/src/index.tsx index d10be77..6bda164 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,19 @@ +// src/index.tsx import React from 'react'; -import ReactDOM from 'react-dom/client'; -import './index.css'; +import ReactDOM from 'react-dom'; import App from './App'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { RecoilRoot } from 'recoil'; +import './index.css'; -const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement -); -root.render( +ReactDOM.render( - - + + + + + + , + + document.getElementById('root') ); diff --git a/src/pages/ChattingList.tsx b/src/pages/ChattingList.tsx new file mode 100644 index 0000000..96c11ea --- /dev/null +++ b/src/pages/ChattingList.tsx @@ -0,0 +1,132 @@ +import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; +import { userListState } from '../state/userState'; +import styled from 'styled-components'; + +const ChatListContainer = styled.div` + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + width: 300px; + +`; + +const ChatItem = styled(Link)` + width: 100%; + padding: 10px; + margin: 5px 0; + background-color: #fffff; + color: black; + text-decoration: none; + border-radius: 5px; + white-space: pre-line; + /*background-image: url('/path/to/background-image.jpg');*/ + display: flex; + align-items: center; + + + &:hover { + background-color: #fffff; + } +`; + +const ProfileImage = styled.img` + width: 50px; + height: 50px; + border-radius: 50%; + margin-right: 20px; +`; + +const ChatDetails = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + width: 100%; + +`; + +const LastMessage = styled.div` + display: flex; + justify-content: space-between; + width: 100%; + font-size: 14px; + color: grey; +`; + +const MessageTime = styled.span` + font-size: 12px; + color: darkgray; + margin-left: auto; + margin-right: 5px; +`; + +const SearchInput = styled.input` + background-image: url('/item/searchbar.png'); + background-repeat: no-repeat; + width: 260px; + height: 21px; + padding: 10px 43px; + margin-bottom: 0px; + margin-left: 5px; + border: none; +`; + +const getLastMessageAndTime = (userId: string) => { + const messagesString = localStorage.getItem(`messages_${userId}`); + if (messagesString) { + const messages = JSON.parse(messagesString); + if (messages.length > 0) { + const lastMessage = messages[messages.length - 1]; + const messageTime = new Date(lastMessage.date).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + hour12: true + }); + return { content: lastMessage.content, time: messageTime }; + } + } + return { content: '๋Œ€ํ™”๋ฅผ ์‹œ์ž‘ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.', time: '' }; +}; + +const ChattingList = () => { + const userList = useRecoilValue(userListState); + const [searchTerm, setSearchTerm] = useState(''); + + const filteredUsers = userList + .filter((user) => + user.name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + .slice(1); // ์ฒซ ๋ฒˆ์งธ ์œ ์ €๋ฅผ ์ œ์™ธํ•˜๊ณ  ํ•„ํ„ฐ๋ง + + return ( + <> + setSearchTerm(e.target.value)} + /> + + {filteredUsers.map((user) => ( + + + +
{user.name}
+ + {getLastMessageAndTime(user.id).content} + {getLastMessageAndTime(user.id).time} + +
+
+ ))} +
+ + ); +}; + +export default ChattingList; diff --git a/src/pages/ChattingRoom.tsx b/src/pages/ChattingRoom.tsx new file mode 100644 index 0000000..38b217c --- /dev/null +++ b/src/pages/ChattingRoom.tsx @@ -0,0 +1,120 @@ +import React, { useEffect, useId, useState, useRef } from 'react'; +import { useParams } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; +import { userListState } from '../state/userState'; +import { userProfileState } from '../state/userProfileState'; +import { messagesState } from '../state/messageState'; +import ChatHead from '../components/ChatHead'; +import ChatBody from '../components/ChatBody'; +import ChatBottom from '../components/ChatBottom'; +import { ChatRoomContainer } from '../style/ChattingRoomStyle'; +import styled from 'styled-components'; + +interface Message { + id: number; + from: string; + to: string; + content: string; + date: string; +} + +const ChatBottomMask = styled.div` + position: fixed; + bottom: 0; + width: 100%; // ๋ทฐํฌํŠธ ์ „์ฒด ๋„ˆ๋น„ ์‚ฌ์šฉ + height: 360px; // ChatBottom์˜ ๋†’์ด์™€ ๋™์ผํ•˜๊ฒŒ ์„ค์ • + background-color: white; // ํฐ์ƒ‰ ๋ฐฐ๊ฒฝ + z-index: 1; // ChatBottom๋ณด๋‹ค ๋‚ฎ์€ z-index ์„ค์ • +`; + +const ChattingRoom = () => { + const { userId } = useParams<{ userId: string }>(); + console.log(useId); + + const messagesStateValue = useRecoilValue(messagesState); + const [messages, setMessages] = useState([]); + + const messagesEndRef = useRef(null); + + useEffect(() => { + // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์ด์ „ ๋ฉ”์‹œ์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ + const storedMessages = localStorage.getItem(`messages_${userId}`); + if (storedMessages) { + // ์ด์ „ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ์œผ๋ฉด ํŒŒ์‹ฑํ•˜์—ฌ ์ƒํƒœ์— ์„ค์ • + setMessages(JSON.parse(storedMessages)); + } + }, [userId]); // userId๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ดํŽ™ํŠธ ์žฌ์‹คํ–‰ + + const userList = useRecoilValue(userListState); + // ๋Œ€ํ™” ์ƒ๋Œ€๋ฐฉ + const otherUser = userList.find((user) => user.id === userId); + + const otherUserIndex = Number(otherUser?.id); + + const [currentUserIndex, setCurrentUserIndex] = useState(0); + const [counterUserIndex, setCounterUserIndex] = useState(otherUserIndex); + + let currentUser = userList[currentUserIndex]; + let counterUser = userList[counterUserIndex]; + + + useEffect(() => { + // ๋ฉ”์‹œ์ง€๊ฐ€ ์ถ”๊ฐ€๋  ๋•Œ๋งˆ๋‹ค ์Šคํฌ๋กค์„ ํ•˜๋‹จ์œผ๋กœ ์ด๋™ + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages, currentUserIndex, counterUserIndex]); + + // ๋ฉ”์„ธ์ง€ ๋ชฉ๋ก ๋์œผ๋กœ ์Šคํฌ๋กค + + // ์œ ์ € ๊ด€๋ฆฌ + // ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ƒํƒœ ๊ด€๋ฆฌ + + + console.log(currentUser); + + //console.log(otherUserIndex); + + const toggleUser = () => { + setCurrentUserIndex(counterUserIndex); + setCounterUserIndex(currentUserIndex); + }; + + const sendMessage = (content: string, from: string, to: string) => { + const newMessage: Message = { + id: Date.now(), + from, + to, + content, + date: new Date().toISOString(), + }; + + // ์ด์ „ ๋ฉ”์‹œ์ง€ ๋ฐฐ์—ด๊ณผ ์ƒˆ๋กœ์šด ๋ฉ”์‹œ์ง€๋ฅผ ํ•ฉ์นจ + const updatedMessages = [...messages, newMessage]; + setMessages(updatedMessages); + + // localStorage์— ์—…๋ฐ์ดํŠธ๋œ ๋ฉ”์‹œ์ง€ ์ €์žฅ + localStorage.setItem(`messages_${userId}`, JSON.stringify(updatedMessages)); + }; + + return ( + <> + + + +
+ + sendMessage(content, currentUser?.id ?? '', counterUser?.id ?? '') + } + /> + + ); +}; + +export default ChattingRoom; diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx new file mode 100644 index 0000000..d501fcc --- /dev/null +++ b/src/pages/Profile.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { useRecoilState } from 'recoil'; +import { userProfileState } from '../state/userProfileState'; +import styled from 'styled-components'; + +interface Profile { + name: string; + phone: string; + image: string; + github: string; + instagram: string; +} + +const ProfileContainer = styled.div` +margin-left:10px; +position:fixed; +widfht:375px; +display: flex; +flex-direction: column; +align-items: center; +/* padding: 20px; + display: flex; + flex-direction: column; + align-items: center;*/ + +`; + +const ProfileImage = styled.img` + width: 100px; + height: 100px; + border-radius: 50%; + margin-bottom: 20px; +`; + +const ProfileFieldName = styled.div` + margin: 10px 0; + text-align: center; +font-family: "Noto Sans KR"; +font-size: 24px; +font-style: normal; +font-weight: 500; +line-height: normal; +letter-spacing: 0.33px; +`; + +const ProfileFieldPhone = styled.div` +color: var(--gray, #8D8D8F); +text-align: center; +font-family: "Noto Sans KR"; +font-size: 15px; +font-style: normal; +font-weight: 400; +line-height: normal; +letter-spacing: -0.2px; +`; + +const Input = styled.input` + padding: 10px; + margin-top: 5px; + width: 200px; + border-radius: 5px; + border: 1px solid #ccc; +`; + +const Button = styled.button` + padding: 10px 20px; + margin-top: 20px; + background-color: #4caf50; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; +`; + +// ๋ฐ”๋กœ๊ฐ€๊ธฐ ๋งํฌ๋ฅผ ๊ฐ์‹ธ๋Š” ์Šคํƒ€์ผ๋œ ์ปดํฌ๋„ŒํŠธ +const ShortcutLink = styled.a` + display: flex; + align-items: center; + text-decoration: none; + color: #333; + margin-top: 10px; +`; + +// ๋กœ๊ณ  ์ด๋ฏธ์ง€ ์Šคํƒ€์ผ๋ง +const InstagramLogo = styled.img` + /* width: 30px; +height: 30px; +margin-right: 10px; */ +`; + +const GitHubLogo = styled.img` + /* width: 30px; +height: 30px; +margin-right: 10px; */ +`; + +const Profile = () => { + const [profile, setProfile] = useRecoilState(userProfileState); + + const handleChange = (event: React.ChangeEvent) => { + const { name, value } = event.target; + setProfile((prev) => ({ ...prev, [name]: value })); + }; + + return ( + + + +
{profile.name}
+
+ +
{profile.phone}
+
+ + + + + + +
+ ); +}; + +export default Profile; diff --git a/src/pages/userList.tsx b/src/pages/userList.tsx new file mode 100644 index 0000000..7483d82 --- /dev/null +++ b/src/pages/userList.tsx @@ -0,0 +1,73 @@ +import React, { useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { userListState } from '../state/userState'; +import styled from 'styled-components'; + +interface User { + id: string; + name: string; + profileImage: string; // ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ URL์„ ์ถ”๊ฐ€ +} + +// ์Šคํƒ€์ผ ์ปดํฌ๋„ŒํŠธ +const UserContainer = styled.li` + background-image: url('/item/userbg.png'); // ์—ฌ๊ธฐ์— ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ URL์„ ์„ค์ • + background-size: cover; + display: flex; + align-items: center; + padding: 5px 0px 5px 0px; + margin-bottom: 11px; + width:300px; + margin-left:-10px; + +`; + +const ProfileImage = styled.img` + width: 45px; + height: 45px; + border-radius: 50%; + margin-right: 20px; + margin-left:-20px; +`; + +const SearchInput = styled.input` + background-image: url('/item/searchbar.png'); + background-repeat: no-repeat; + width: 260px; + height: 21px; + padding: 10px 43px; + margin-bottom: 0px; + margin-left: 5px; + border: none; +`; + +const UserList = () => { + const users = useRecoilValue(userListState); + const [searchTerm, setSearchTerm] = useState(''); + + const filteredUsers = users + .filter((user) => + user.name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + .slice(1); // ์ฒซ ๋ฒˆ์งธ ์œ ์ €๋ฅผ ์ œ์™ธํ•˜๊ณ  ํ•„ํ„ฐ๋ง + + return ( + <> + setSearchTerm(e.target.value)} + /> +
    + {filteredUsers.map((user) => ( + + + {user.name} + + ))} +
+ + ); +}; + +export default UserList; diff --git a/src/state/chatListState.ts b/src/state/chatListState.ts new file mode 100644 index 0000000..094f6ad --- /dev/null +++ b/src/state/chatListState.ts @@ -0,0 +1,19 @@ +// src/state/chatListState.ts + +import { atom } from 'recoil'; + +interface ChatRoom { + id: string; + partner: string; + recentMsg: number; +} + +export const chatListState = atom({ + key: 'chatListState', + default: [ + { id: 'room1', partner: '๊น€๋ชจ์”จ', recentMsg: 3 }, + { id: 'room2', partner: '๋ฐ•๋ชจ์”จ', recentMsg: 1 }, + { id: 'room3', partner: '์ตœ๋ชจ์”จ', recentMsg: 1 }, + // ์ถ”๊ฐ€ ๋Œ€ํ™”๋ฐฉ ์ •๋ณด + ], +}); \ No newline at end of file diff --git a/src/state/messageState.ts b/src/state/messageState.ts new file mode 100644 index 0000000..1a79adc --- /dev/null +++ b/src/state/messageState.ts @@ -0,0 +1,16 @@ +// src/state/messageState.ts + +import { atom } from 'recoil'; + +interface Message { + id: number; + from: string; + to: string; + content: string; + date: string; +} + +export const messagesState = atom<{ [key: string]: Message[] } | undefined>({ + key: 'messagesState', + default: undefined, +}); diff --git a/src/state/userProfileState.ts b/src/state/userProfileState.ts new file mode 100644 index 0000000..d1c263e --- /dev/null +++ b/src/state/userProfileState.ts @@ -0,0 +1,14 @@ +// src/state/userProfileState.ts + +import { atom } from 'recoil'; + +export const userProfileState = atom({ + key: 'userProfileState', + default: { + name: '์ด์˜์ธ', + phone: '82) 10-1234-5678', + image: '/item/profile_mini2.png', + github: 'https://github.com/jinnyleeis', + instagram: 'https://www.instagram.com/', + }, +}); diff --git a/src/state/userState.ts b/src/state/userState.ts new file mode 100644 index 0000000..a0fd3dc --- /dev/null +++ b/src/state/userState.ts @@ -0,0 +1,60 @@ +// src/state/userState.ts + +import { atom } from 'recoil'; + +export interface Message { + id: number; + from: string; + to: string; + content: string; + date: string; +} + +interface User { + id: string; + name: string; + image: string; + chatHistory?: Message[]; // ๋Œ€ํ™” ๋‚ด์—ญ ์ˆ˜์ • +} + +export const userListState = atom({ + key: 'userListState', + default: [ + // 0๋ฒˆ์€ current user - ๋ฉ”์‹ ์ € ์•ฑ ์ฃผ์ธ์œผ๋กœ ์„ค์ • + { + id: '0', + name: '์ด์˜์ธ', + image: '/item/profile_mini2.png', + chatHistory: [], + }, + { + id: '1', + name: '๊น€๋ชจ์”จ', + image: '/item/profile_mini.png', + chatHistory: [], + }, + { + id: '2', + name: '๋ฐ•๋ชจ์”จ', + image: '/item/profile_mini.png', + chatHistory: [], + }, + { + id: '3', + name: '์ตœ๋ชจ์”จ', + image: '/item/profile_mini.png', + chatHistory: [], + }, + { + id: '4', + name: '์ตœ๋ชจ์”จ', + image: '/item/profile_mini.png', + chatHistory: [], + }, + ], +}); + +export const currentUserState = atom({ + key: 'currentUserState', + default: { id: '0', name: '๊น€๋ชจ์”จ', image: '', chatHistory: [] }, +}); diff --git a/src/style/ChatBodyStyle.tsx b/src/style/ChatBodyStyle.tsx new file mode 100644 index 0000000..b583baa --- /dev/null +++ b/src/style/ChatBodyStyle.tsx @@ -0,0 +1,85 @@ +import styled, { css } from 'styled-components'; + +export const ChatMessageBox = styled.div<{ isCurrentUser: boolean }>` + display: inline-block; + padding: 8px 9px 8px 9px; + + width: 196px; + margin-left: 30px; + margin-bottom: 5px; + margin-top: 5px; + border-radius: 8px; + background-color: #ededed; + align-self: flex-start; + border-radius: 20px 20px 20px 5px; + + color: var(--dark-gray, #4e5058); + font-family: 'Noto Sans KR'; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; /* 24px */ + letter-spacing: -0.41px; + + ${(props) => + props.isCurrentUser && + css` + border-radius: 20px 20px 5px 20px; + background-color: #dcf7c5; + align-self: flex-end; + margin-right: 5px; + `} + + ${(props) => !props.isCurrentUser && css``} +`; + +export const ChatBodyContainer = styled.div` + display: flex; + flex-direction: column; + margin-top: 96px; + + margin-bottom: 400px; + width: 375px; +`; + +export const UserProfileImage = styled.img<{ isCurrentUser: boolean }>` + width: 20px; + height: 20px; + border-radius: 50%; + margin-right: 0px; /* ๋ฉ”์‹œ์ง€ ๋ฐ•์Šค์™€์˜ ๊ฐ„๊ฒฉ */ + margin-left: 5px; /* ๋ฉ”์‹œ์ง€ ๋ฐ•์Šค์™€์˜ ๊ฐ„๊ฒฉ */ + display: inline-block; + margin-top: -10px; + position: relative; + z-index: -5; + + ${(props) => + props.isCurrentUser && + css` + display: none; + `} +`; + +export const DateSeparator = styled.div` + width: 122px; + height: 21px; + flex-shrink: 0; + border-radius: 4px; + background: var(--medium-gray, #d9d9d9); + color: #000; + text-align: center; + margin: 10px auto; // ์ •์ค‘์•™ ์ •๋ ฌ + font-family: 'Noto Sans KR'; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 180%; /* 18px */ +`; + +export const TimeDisplay = styled.span` + display: block; // ๋ฉ”์‹œ์ง€ ๋ฐ•์Šค ์œ„์— ํ‘œ์‹œ + text-align: center; // ์ •์ค‘์•™ ์ •๋ ฌ + margin-bottom: 5px; + margin-top: 5px; + font-size: 12px; +`; diff --git a/src/style/ChatBottomStyle.tsx b/src/style/ChatBottomStyle.tsx new file mode 100644 index 0000000..29e2787 --- /dev/null +++ b/src/style/ChatBottomStyle.tsx @@ -0,0 +1,39 @@ +import styled from 'styled-components'; + +// ChatBottomContainer๋ฅผ div๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์ปจํ…Œ์ด๋„ˆ๋กœ ์‚ฌ์šฉ +export const ChatBottomContainer = styled.div` + position: fixed; + bottom: 280px; + width: 375px; + height: 80px; + flex-shrink: 0; + + background-image: url('/item/chatBottom.png'); + background-size: cover; + z-index: 2; +`; + +export const ChatInput = styled.input` + width: 275px; + height: 32px; + flex-shrink: 0; + font-size: 14px; + margin-left: 11%; + border-radius: 16px; + border: 0.5px solid #8e8e93; + padding: 0px 0px 0px 7px; + opacity: 0.4496; + margin-top: 8px; + background: #fff; +`; + +export const SendButton = styled.img` + width: 30px; + height: 30px; + flex-shrink: 0; + cursor: pointer; + border: none; + position: absolute; + margin-top: 9px; + margin-left: 4px; +`; diff --git a/src/style/ChatHeadStyle.tsx b/src/style/ChatHeadStyle.tsx new file mode 100644 index 0000000..a0c4346 --- /dev/null +++ b/src/style/ChatHeadStyle.tsx @@ -0,0 +1,44 @@ +import styled from 'styled-components'; + +export const ChatHeadContainer = styled.div` + width: 375px; + height: 96px; + position: fixed; + top: 0px; + display: flex; + justify-content: flex-start; + align-items: center; + + flex-shrink: 0; + background-image: url('/item/chatHead.png'); + background-size: cover; + +`; + +export const UserImage = styled.img` + margin-left: 45px; + margin-right: 10px; + margin-top: 30px; + border-radius: 50%; + + width: 10px; + height: 40px; +`; + +export const UserName = styled.h2` + font-size: 16px; + margin-top: 47px; + font-style: normal; + font-weight: 500; +`; + +export const SendButton = styled.img` + width: 25px; + height: 25px; + flex-shrink: 0; + cursor: pointer; + border: none; + position: absolute; + margin-top: 30px; + margin-left: 10px; +`; diff --git a/src/style/ChattingRoomStyle.tsx b/src/style/ChattingRoomStyle.tsx new file mode 100644 index 0000000..5eb6497 --- /dev/null +++ b/src/style/ChattingRoomStyle.tsx @@ -0,0 +1,10 @@ +import styled from 'styled-components'; + +export const ChatRoomContainer = styled.div` + width: 375px; + height: 212px; + background: var(--white, #fff); + display: flex; + + +`; diff --git a/src/style/GlobalStyle.tsx b/src/style/GlobalStyle.tsx new file mode 100644 index 0000000..bc89290 --- /dev/null +++ b/src/style/GlobalStyle.tsx @@ -0,0 +1,10 @@ +import { createGlobalStyle } from 'styled-components'; + +const GlobalStyle = createGlobalStyle` + +*{font-family: "Noto Sans KR"}; + + +`; + +export default GlobalStyle; diff --git a/tsconfig.json b/tsconfig.json index a273b0c..7c03434 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,26 +1,20 @@ { - "compilerOptions": { - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx" - }, - "include": [ - "src" - ] + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + // "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] }