Skip to content

Epikoding/kmongCloneCoding_Back

ย 
ย 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐ŸŽกย ํฌ๋ชฝ(kmong) - ํด๋ก ์ฝ”๋”ฉ


๐ŸŽ‘ย ํด๋ก ์ฝ”๋”ฉ์œผ๋กœ ํฌ๋ชฝ์„ ์„ ํƒํ•˜๊ฒŒ ๋œ ๋ชฉ์  & ๊ฐœ๋ฐœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ

๋ชฉ์  ์„ค๋ช…

์œ ์ € ๋“œ๋ฆฌ๋ธ(User driven)ํ•˜๋ฉด์„œ ํ”„๋กœ๋ฐ”์ด๋” ๋“œ๋ฆฌ๋ธ(provider driven)ํ•œ ์›นํŽ˜์ด์ง€๋ฅผ ์ œ์ž‘ํ•˜๊ณ ์ž ํฌ๋ชฝ์„ ์„ ํƒํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๊ฐœ๋ฐœ์ž์˜ ๊ธฐ๋ณธ์ ์ธ ์—ญ๋Ÿ‰์„ ํ‚ค์šฐ๋ ค๋Š” ๊ฒƒ๊ณผ ๋™์‹œ์— ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ๊ตฌํ˜„์„ ํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œํ•œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.

๋ถ€ํŠธ์บ ํ”„ ํ•ญํ•ด99์˜ ์ฒ˜์Œ ํ•œ ๋‹ฌ์ด ์กฐ๊ธˆ ๋„˜์€ ์‹œ๊ฐ„๋™์•ˆ ์ˆ˜๊ฐ•์ƒ๋“ค์€ ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ ๊ทธ๋ฆฌ๊ณ  CRUD ๊ธฐ๋Šฅ์„ ์ค‘์ ์ ์œผ๋กœ ๋‹ค๋ฃน๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ์— ํด๋ก ์— ๋Œ€ํ•œ ๋ชฉํ‘œ๋กœ ์ธ์Šคํƒ€๊ทธ๋žจ, ์Šฌ๋ž™, ๋‹น๊ทผ๋งˆ์ผ“๊ณผ ๊ฐ™์€ ์œ ์ €์™€ ํ”„๋กœ๋ฐ”์ด๋”๊ฐ€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ์„œ๋น„์Šค๋ฅผ ํด๋ก ํ•˜๋Š” ๊ฒฝํ–ฅ์ด ์ƒ๊น๋‹ˆ๋‹ค. ์ด์— ๋Œ€ํ•œ ์ฐธ๊ณ ์ž๋ฃŒ๋กœ์„œ ์•ž์„  ํ•ญํ•ด99 ์ˆ˜๊ฐ•์ƒ๋“ค์˜ ์ž‘ํ’ˆ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

ํŒ€์›๋“ค๊ณผ ๊ธฐ๋‚˜๊ธด ๋…ผ์˜๋ฅผ ๋์œผ๋กœ ์ €ํฌ ํŒ€์€ ์ด์ „ ์ˆ˜๊ฐ•์ƒ๋“ค์ด ํ•˜์ง€ ์•Š์€ ํด๋ก ์˜ ๋ชฉ์ ์œผ๋กœ โ€˜ํฌ๋ชฝ'์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋Œ€์„œ์–‘์„ ๊ฑด๋„œ๋˜ ์ฝœ๋กฌ๋ฒ„์Šค์˜ ๋งˆ์Œ์„ ๊ฐ€์ ธ๋ณด๊ธฐ๋กœ ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ฐธ๊ณ ์ž๋ฃŒ ๋งŽ์ง€ ์•Š๊ธฐ์— ๋ง‰๋ง‰ํ•˜์ง€๋งŒ ์ž‘์€ ์„ฑ๊ณต์„ ๊ฟˆ๊พธ๊ธฐ์— ํฌ๋ชฝ์€ ์ €ํฌ์—๊ฒŒ ์‹คํ˜„ ๊ฐ€๋Šฅํ•œ ๋ชฉํ‘œ์ฒ˜๋Ÿผ ๋ณด์˜€์Šต๋‹ˆ๋‹ค.

๊ฐœ๋ฐœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ

ํฌ๋ชฝ์˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋งŽ์€ ๊ด€๊ณ„๋กœ โ€˜ํฌ๋ชฝ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ(kmong.com/enterprise)โ€™์„ ํด๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ตฌํ˜„๋œ ๊ธฐ๋Šฅ์˜ ๊ฐ„๋‹จํ•œ ๊ฐœ์š”๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. โ€˜ํ”„๋กœ์ ํŠธ ์˜๋ขฐํ•˜๊ธฐโ€™ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ, ์š”์†Œ ์„ ํƒ ํ›„ ๋“ฑ๋ก.
  2. ์˜๋ขฐ ๋œ ํ”„๋กœ์ ํŠธ๋“ค์˜ ๋ฆฌ์ŠคํŠธ ๋ฐ˜ํ™˜.
  3. ํšŒ์›๊ฐ€์ž… ์ง„ํ–‰๊ณผ ๋กœ๊ทธ์ธ.

๋ณด๋‹ค ์ž์„ธํ•œ ์‚ฌํ•ญ์€ ํ•˜๊ธฐ โ€˜**3. Wireframe - ํฌ๋ชฝโ€™**์—์„œ ์ฐธ๊ณ ํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ‘ฅย 1. ์ œ์ž‘ ๊ธฐ๊ฐ„ & ํŒ€์› ์†Œ๊ฐœ

  • 2022๋…„ 06์›” 16์ผ ~ 2022๋…„ 06์›” 23์ผ
์ด๋ฆ„ ๊ฐœ์ธ ๋ธ”๋กœ๊ทธ ๋งํฌ ๊นƒํ—ˆ๋ธŒ ๋งํฌ ํ”„๋ก ํŠธ&๋ฐฑ์—”๋“œ
์ด๊ฐ€์—ฐ https://2022gygy.tistory.com/ https://github.com/gygy2022 ํ”„๋ก ํŠธ
์กฐํ•ด์†” https://velog.io/@solpine https://github.com/sol-pine ํ”„๋ก ํŠธ
ํ•œ์ง€์šฉ https://velog.io/@jigom https://github.com/jigomgom ํ”„๋ก ํŠธ
์ด๋™์žฌ https://velog.io/@djlesque https://github.com/Epikoding ๋ฐฑ์—”๋“œ
๋ฐ•์„ธ์—ด https://park-se-yeol.tistory.com/ https://park-se-yeol.tistory.com/ ๋ฐฑ์—”๋“œ
๊น€๋ฏผ์ง€ https://velog.io/@alswlwkd20 https://github.com/minji-kim525 ๋ฐฑ์—”๋“œ

์กฐ์› ์—ญํ•  ๋ฐ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ ์„ค๋ช…

์กฐํ•ด์†”

  • Project List view, Detail view, Create view ์ž‘์—…
  • ํ”„๋กœ์ ํŠธ ์˜๋ขฐ ์ƒ์„ฑ ๊ตฌํ˜„
  • ํ”„๋กœ์ ํŠธ ์˜๋ขฐ ํŒŒ์ผ ์ฒจ๋ถ€ ๋ฐ ์ธ๋„ค์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ตฌํ˜„

ํ•œ์ง€์šฉ

  • Main view ์ž‘์„ฑ
  • ์„œ๋ฒ„๊ฐ„ ํ†ต์‹  ํ…Œ์ŠคํŠธ ๋ฐ ์‚ฌ์ „ ๊ฐ€์ด๋“œ๋ผ์ธ ์ž‘์„ฑ
  • ๋กœ๊ทธ์ธ, ํšŒ์›๊ฐ€์ž…, ์˜๋ขฐ ์ˆ˜์ • ๋ฐ ์‚ญ์ œ ๊ตฌํ˜„

์ด๊ฐ€์—ฐ

  • Login view, SignUp view, MyKmong view ์ž‘์—…
  • ์„œ๋ฒ„์—์„œ ๋ฐ›์•„์˜ค๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ํ™”๋ฉด์— ๋ฟŒ๋ฆฌ๋Š” ์ž‘์—…
  • ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ตฌํ˜„

์ด๋™์žฌ

  • ๋กœ๊ทธ์ธ & ํšŒ์›๊ฐ€์ž… ๊ธฐ๋Šฅ ๊ตฌํ˜„
  • security ์„ค์ •

๋ฐ•์„ธ์—ด

  • ํ™ˆ ํ™”๋ฉด ์กฐํšŒ ๊ธฐ๋Šฅ ๋ฐ ํ”„๋กœ์ ํŠธ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ ๊ธฐ๋Šฅ
  • ํŒŒ์ผ ์—…๋กœ๋“œ ๋ฐ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ, ์†Œ์…œ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ

๊น€๋ฏผ์ง€

  • ํ”„๋กœ์ ํŠธ CRUD, ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ
  • MVC ํŒจํ„ด ์„ค๊ณ„

๐Ÿงฐย 2. ์‚ฌ์šฉ ๊ธฐ์ˆ  ๋ฐ ํˆด

๐ŸŒฑย Back-end ๐ŸŒฑ

  • Java 8
  • SpringBoot
  • Spring Security
  • Gradle
  • JPA
  • MySQL 8.0
  • AWS S3
  • JWT
  • OAuth2

๐ŸŒฑย Front-end ๐ŸŒฑ

  • React
  • react-router-dom
  • Axios
  • Redux
  • Styeld Component (for es6 and css)
  • Fortawesome
  • redux-toolkit

๐ŸŒฑย ๋ฐฐํฌ ๐ŸŒฑ

  • AWS
  • FileZilla

๐Ÿ–‡๏ธย 3. Wireframe - ํฌ๋ชฝ

๋ฉ”์ธํ™”๋ฉด

ํ”„๋กœ์ ํŠธ ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€

ํšŒ์›๊ฐ€์ž…

๋กœ๊ทธ์ธ

์˜๋ขฐ ์ƒ์„ฑํ•˜๊ธฐ

์ƒ์„ธ ํŽ˜์ด์ง€

๋งˆ์ดํฌ๋ชฝ


๐Ÿ–‡๏ธย 4. S.A (Starting Assignment)

https://docs.google.com/spreadsheets/d/1xkkSbZWIB8ChC1NRSAErkekziXt4bmB7EdIGHJpftzs/edit#gid=1824601528


๐Ÿ–‡๏ธย 5. ์‹คํ–‰ํ™”๋ฉด ์œ ํŠœ๋ธŒ

https://www.youtube.com/watch?v=aTwMly1ICzE


๐Ÿ’ฏย 6. ํ•ต์‹ฌ๊ธฐ๋Šฅ

  • ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ & ๋กœ๊ทธ์•„์›ƒ

    • jwt์„ ์‚ฌ์šฉํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
    • ์ด๋ฉ”์ผ ์–‘์‹ ์ •๊ทœ์‹์œผ๋กœ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
  • ํ”„๋กœ์ ํŠธ CRUD

    • ๊ฐ ํ”„๋กœ์ ํŠธ๋งˆ๋‹ค ์ƒ์„ธ ํŽ˜์ด์ง€ ๊ตฌ์„ฑ ํ›„ ์„ธ๋ถ€ ์‚ฌํ•ญ ํ™•์ธ
    • ์ •๋ ฌ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ํ‚ค์›Œ๋“œ ๋ณ„๋กœ ํ™•์ธ ๊ฐ€๋Šฅ
  • ํ™ˆํŽ˜์ด์ง€์™€ ํ”„๋กœ์ ํŠธ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ

    • ๊ฐ ์ƒํ™ฉ์— ๋งž๋Š” ์ •๋ณด ์ „๋‹ฌ
  • ํŒŒ์ผ ์—…๋กœ๋“œ

    • ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํŒŒ์ผ๋“ค์„ ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ๋กœ ์ „๋‹ฌ ๋ฐ›์•„ s3์— ์ €์žฅ
    • ์ดํ›„ ์„œ๋ฒ„์—์„œ s3 URL์„ ๋‚œ์ˆ˜ํ™”๋œ ํŒŒ์ผ ์ด๋ฆ„๊ณผ ํ•จ๊ป˜ ์ €์žฅ
    • ์ƒ์„ธ ํŽ˜์ด์ง€์—์„œ ํŒŒ์ผ ํ™•์ธ ๊ฐ€๋Šฅ
  • ํŒŒ์ผ ์—…๋กœ๋“œ(2)

    • ํ”„๋ก ํŠธ์—์„œ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๊ธฐ ์œ„ํ•ด JSON.stringfy์™€ Blob์œผ๋กœ 2์ง„ํ™” ๋ฐ์ดํ„ฐ ์ƒ์„ฑ

๐ŸŽฎย 7. Trouble shooting

Front-end

Redux์˜ state( 1 )

์ด์Šˆ ๋‚ด์šฉ : Redux์˜ state ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ๋•Œ ๊ฐฑ์‹  ์ „ ๊ฐ’์„ ์ฝ์–ด์™€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• : initialState์— ๊ฐ’์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ด€๋ฆฌ, useEffect์— dependency๋ฅผ ๋‘์–ด ์ธ์ž๊ฐ’์ด ๋ฐ”๋€” ๋•Œ useState๋กœ ๊ฐ’์„ ์ธ์ง€

Redux์˜ state( 2 )

์ด์Šˆ ๋‚ด์šฉ : ๋””ํ…Œ์ผ ํŽ˜์ด์ง€ ๋กœ๋”ฉ๊ณผ ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ์ „๋‹ฌ๋ฐ›์€ ๋ฐ์ดํ„ฐ ๊ฐ’์ด ๋“ค์–ด์˜ค๋Š” ๊ฒƒ์— ๋”œ๋ ˆ์ด๊ฐ€ ์žˆ์–ด ๋กœ๋”ฉ ์‹œ, ๋ฐ์ดํ„ฐ ๋ฐฐ์—ด์˜ ์ธ๋ฑ์Šค์— ์ ‘๊ทผํ•˜์ง€ ๋ชปํ•ด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• : ๋ฆฌ๋•์Šค์— ๋ฐฐ์—ด์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ๊ณ  ์Šคํ† ์–ด์—์„œ ๋นผ์˜ค๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐ

Radio button checked ์†์„ฑ

์ด์Šˆ ๋‚ด์šฉ : radio button์˜ checked ์†์„ฑ์„ ์ „๋‹ฌํ•˜๋Š” ๋ฌธ์ œ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• : Radio button ์œผ๋กœ ๋ฌถ์ธ ๋ถ€๋ถ„์€ map์„ ํ™œ์šฉํ•˜์—ฌ ์ƒ์„ฑํ•˜๊ณ  useState๋ฅผ ํ†ตํ•ด checked ์—ฌ๋ถ€๋ฅผ ์ˆ˜์ •

๋ฐ์ดํ„ฐ parsing

์ด์Šˆ ๋‚ด์šฉ : ์„œ๋ฒ„์—์„œ ๋ฐ›์•„์˜จ Data๋ฅผ ์ถ”์ถœ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• : dictionary๋กœ ๋ฌถ์ธ ๋ถ€๋ถ„์„ ๋ฏธ๋ฆฌ ์„ ์–ธํ•ด๋‘” ๋ฐฐ์—ด์—์„œ some๊ณผ filter๋ฅผ ์ด์šฉํ•˜์—ฌ ์ค‘๋ณต๋˜๋Š” ๋ถ€๋ถ„์„ ์ถ”์ถœํ•˜์—ฌ ํ™œ์šฉ

ํŒŒ์ผ ์—…๋กœ๋“œ

์ด์Šˆ ๋‚ด์šฉ : formdata ๋‚ด๋ถ€์— ๋”•์…”๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ appendํ•˜์—ฌ ์ „๋‹ฌํ•  ๋•Œ 400 ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• : ****JSON.stringify ๋กœ ๋ณ€ํ™˜ ํ›„ appendํ•˜์—ฌ ํ•ด๊ฒฐ

Back-end

editProject์—์„œ response ๊ฐ’

์ด์Šˆ ๋‚ด์šฉ : ์ˆ˜์ •ํ•  ๋•Œ, ์ค‘๋ณต์ฒดํฌ์—์„œ String์œผ๋กœ ๋ฐ›์€ ๊ฐ’์„ ํ”„๋ก ํŠธ์— ์–ด๋–ป๊ฒŒ response ํ•ด์ค„์ง€์— ๋Œ€ํ•œ ์ด์Šˆ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• : โ€œ,โ€ ๋กœ splitํ•œ ๋‹ค์Œ, map์œผ๋กœ string(key):true(value)๋กœ ๋ณด๋‚ด๋Š” ๊ฒƒ์œผ๋กœ ๊ฒฐ์ •

modal ์ ‘๊ทผ ๋ถˆ๊ฐ€

์ด์Šˆ ๋‚ด์šฉ: projects/modal๋กœ ํ–ˆ์„ ๋•Œ ์ ‘๊ทผ์ด ์•ˆ ๋˜์—ˆ๋˜ ์ด์Šˆ.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• : List๋Š” ๋ณต์ˆ˜ ๊ฐœ์˜ fetch ๊ฐ€ ์•ˆ๋จ์œผ๋กœ ์ปฌ๋Ÿผ ํƒ€์ž… Set์œผ๋กœ ๋ณ€๊ฒฝ

์˜ฌ๋ฐ”๋ฅธ jwt ํ† ํฐ์ด ์•„๋‹™๋‹ˆ๋‹ค ์—๋Ÿฌ

์ด์Šˆ ๋‚ด์šฉ: ๊ธฐํƒ€ ์กฐํšŒ ๊ธฐ๋Šฅ๋“ค์„ ์‚ฌ์šฉํ•  ๋•Œ 500 internal server error ์ด์Šˆ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• : skipPathList์— GET ๋งคํ•‘ api ์ถ”๊ฐ€

ํ”„๋กœ์ ํŠธ ์ถ”๊ฐ€ ์‹œ ํŒŒ์ผ ์ œ์™ธ

์ด์Šˆ ๋‚ด์šฉ: ๊ธฐ์กด์— ์žˆ๋˜ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ๋Š” ํŒŒ์ผ์„ ๊ผญ ์˜ฌ๋ ค์•ผ ํ–ˆ๋˜ ์ด์Šˆ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• : @RequestPart(value = "files",required = false) required = false ์ถ”๊ฐ€

๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๊ด€๋ จ ํ‚ค์›Œ๋“œ ์กฐํšŒ ์ด์Šˆ

์ด์Šˆ ๋‚ด์šฉ: JPA ํ‚ค์›Œ๋“œ ์ค‘ LIKE(โ€%KEYWORD%โ€)์™€ ๊ฐ™์€ ์—ญํ• ์„ ํ•˜๋Š” ๋ฌธ๋ฒ• ํ•„์š” ์ด์Šˆ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• : findByTitleContainingOrderByCreatedAt(String keyword) ๋ฌธ๋ฒ•์„ ํ†ตํ•ด ํ•ด๊ฒฐ, Containing ๋ฌธ๋ฒ•์ด SQL์˜ LIKE ์—ญํ• ์„ ํ•จ

์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์›์ด ๋กœ๊ทธ์ธ์„ ํ•  ๋•Œ 500์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ.

์ด์Šˆ ๋‚ด์šฉ : JpaRepository๋ฅผ ์ƒ์†๋ฐ›์€ UserRepository์—์„œ Optionalํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•œ User๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์กฐ๊ฑด์—์„œ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์›ID๋กœ ๋กœ๊ทธ์ธ์„ ์‹œ๋„ํ•  ๋•Œ 500์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒํ•จ.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• : UserRepository์—์„œ Optional<User>์„ User๋กœ ๋ณ€๊ฒฝ. ์ž์„ธํ•œ ์„ค๋ช…์€ ํ•˜๊ธฐ โ€˜8-2.์˜ Optional ๊ฐ์ฒดโ€™ ์ฐธ๊ณ .


โš™๏ธย 8. ์ฃผ๋ชฉํ•  ๋งŒํ•œ ์ฝ”๋“œ

8-1. Front-end

๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง

  • filter ์™€ some ์„ ํ†ตํ•ด ์ค‘๋ณต๋œ ๊ฐ’์„ ์ถ”์ถœ

    const Valuelist = [ { ...action.payload.responseDtoMap } ];
          const keylist = Object.keys ( Valuelist[0] );
    
          let resultRequired = keylist.filter( x1 => RequiredFunction.some(x2=> x1 === x2 ) )[0];
          let resultCommerce = keylist.filter( x1 => Commerce.some(x2=> x1 === x2 ) )[0];
          let resultSites = keylist.filter( x1 => Sites.some(x2=> x1 === x2 ) )[0];
          let resultUserRelated = keylist.filter( x1 => userRelated.some(x2=> x1 === x2 ) )[0];

Map์„ ํ†ตํ•œ Radio button Checked ํ™œ์„ฑํ™”

  • checked ๋ฅผ ํ™•์ธ

    const CurrentStatus = ["์•„์ด๋””์–ด๋งŒ ์žˆ์Œ", "๊ธฐํš์„œ ๋ณด์œ ", "๋””์ž์ธ ๋ณด์œ ", "๊ฐœ๋ฐœํ™˜๊ฒฝ ๋ณด์œ "];
    const RequiredFunction = ["๊ฐค๋Ÿฌ๋ฆฌ", "๊ฒŒ์‹œํŒ", "์ผ์ • ๊ด€๋ฆฌ", "SNS ์—ฐ๋™"];
    
    { Commerce.map( ( item, index ) => {
                    return (
                      <Select key={index}>
                        <Input
                          type="radio"
                          name="commerceRelatedFunction"
                          value={item}
                          onChange={handleCommerceRelatedFunction}
                          checked={ setCommerce === item }
                        />
                        <span>{item}</span>
                      </Select>
                    );
                })}

JSON.stringfy์™€ Blob์„ ํ™œ์šฉํ•œ ๋ฐ์ดํ„ฐ 2์ง„ํ™” ์ฒ˜๋ฆฌ

  • ๊ฐ์ฒด๋ฅผ stringfy์™€ Blob์œผ๋กœ ์ฒ˜๋ฆฌ

    const formData = new FormData();
        formData.append(
          "projectDto",
          new Blob(
            [JSON.stringify(projectDto, { contentType: "application/json" })],
            {
              type: "application/json",
            }
          )
        );
        formData.append("files", file);

8-2. Back-end

์ฝ”๋ฉ˜ํŠธ ์ž‘์„ฑ

  • AwsS3Service ์•„๋งˆ์กด ์„œ๋น„์Šค ํŒŒ์ผ์„ ํ†ตํ•ด ์—…๋กœ๋“œ ๋ฐ ์‚ญ์ œ ๊ตฌํ˜„

    @Service
    @RequiredArgsConstructor
    public class AwsS3Service {
    
        @Value("${cloud.aws.s3.bucket}")
        private String bucket;
    
        private finalAmazonS3amazonS3;
        @Transactional
        publicList<FileRequestDto> uploadFile(List<MultipartFile> multipartFile) {
    List<String> fileNameList = new ArrayList<>();
    List<FileRequestDto> fileRequestDtos = new ArrayList<>();
    // forEach๊ตฌ๋ฌธ์„ ํ†ตํ•ด multipartFile๋กœ ๋„˜์–ด์˜จ ํŒŒ์ผ๋“ค ํ•˜๋‚˜์”ฉ fileNameList์— ์ถ”๊ฐ€
    for(MultipartFilefile : multipartFile){
                String fileName = createFileName(file.getOriginalFilename());
                ObjectMetadata objectMetadata = new ObjectMetadata();
                objectMetadata.setContentLength(file.getSize());
                objectMetadata.setContentType(file.getContentType());
    
                try(InputStream inputStream = file.getInputStream()) {
                    amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
                            .withCannedAcl(CannedAccessControlList.PublicRead));
                    fileRequestDtos.add(new FileRequestDto(amazonS3.getUrl(bucket,fileName).toString(),fileName));
    
                } catch(IOException e) {
                    throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "ํŒŒ์ผ ์—…๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.");
                }
            }
            return fileRequestDtos;
        }
        @Transactional
        public void deleteFile(String fileName) {
            amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
        }
    
        private String createFileName(String fileName) {//๋จผ์ € ํŒŒ์ผ ์—…๋กœ๋“œ ์‹œ,ํŒŒ์ผ๋ช…์„ ๋‚œ์ˆ˜ํ™”ํ•˜๊ธฐ ์œ„ํ•ด random์œผ๋กœ ๋Œ๋ฆฝ๋‹ˆ๋‹ค.
    return UUID.randomUUID().toString().concat(getFileExtension(fileName));
        }
    
        private String getFileExtension(String fileName) {// fileํ˜•์‹์ด ์ž˜๋ชป๋œ ๊ฒฝ์šฐ๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ๋งŒ๋“ค์–ด์ง„ ๋กœ์ง์ด๋ฉฐ,ํŒŒ์ผ ํƒ€์ž…๊ณผ ์ƒ๊ด€์—†์ด ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด .์˜ ์กด์žฌ ์œ ๋ฌด๋งŒ ํŒ๋‹จํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    try {
                return fileName.substring(fileName.lastIndexOf("."));
            } catch (StringIndexOutOfBoundsException e) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "์ž˜๋ชป๋œ ํ˜•์‹์˜ ํŒŒ์ผ(" + fileName + ") ์ž…๋‹ˆ๋‹ค.");
            }
        }
    }
  • AmazonS3Config ์ปจํ”ผํ๋ ˆ์ด์…˜์˜ ๋นˆ ๋“ฑ๋ก

    @Configuration
    public class AmazonS3Config {
    
        @Value("${cloud.aws.credentials.access-key}")
        private String accessKey;
    
        @Value("${cloud.aws.credentials.secret-key}")
        private String secretKey;
    
        @Value("${cloud.aws.region.static}")
        private String region;
    
        @Bean
        public AmazonS3Client amazonS3Client() {
            BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
            return (AmazonS3Client) AmazonS3ClientBuilder.standard()
                    .withRegion(region)
                    .withCredentials(new AWSStaticCredentialsProvider(awsCreds))
                    .build();
        }
    }
  • GoogleUserService ๋ฅผ ํ†ตํ•ด ํ”„๋ก ํŠธ์—์„œ ๊ตฌ๊ธ€ ํด๋ผ์ด์–ธํŠธ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ,

    ์ดํ›„ ๋กœ๊ทธ์ธ ๋ฐ ํšŒ์›๊ฐ€์ž…์„ ์ง„ํ–‰ํ•˜๊ณ  jwtํ† ํฐ ๋ฐœ๊ธ‰

    @Service
    @RequiredArgsConstructor
    public class GoogleUserService {
    
        private final PasswordEncoder passwordEncoder;
        private final UserRepository userRepository;
    
        @Value("${spring.security.oauth2.client.registration.google.client-id}")
        private String clientId;
    
        @Value("${spring.security.oauth2.client.registration.google.client-secret}")
        private String clientSecret;
    
        public GoogleUserResponseDto googleLogin(String code) throws JsonProcessingException {
            //HTTP Request๋ฅผ ์œ„ํ•œ RestTemplate
            RestTemplate restTemplate = new RestTemplate();
    
            // 1. "์ธ๊ฐ€ ์ฝ”๋“œ"๋กœ "์•ก์„ธ์Šค ํ† ํฐ" ์š”์ฒญ
            String accessToken = getAccessToken(restTemplate, code);
    
            // 2. "์•ก์„ธ์Šค ํ† ํฐ"์œผ๋กœ "๊ตฌ๊ธ€ ์‚ฌ์šฉ์ž ์ •๋ณด" ๊ฐ€์ ธ์˜ค๊ธฐ
            GoogleUserInfoDto snsUserInfoDto = getGoogleUserInfo(restTemplate, accessToken);
    
            // 3. "๊ตฌ๊ธ€ ์‚ฌ์šฉ์ž ์ •๋ณด"๋กœ ํ•„์š”์‹œ ํšŒ์›๊ฐ€์ž…  ๋ฐ ์ด๋ฏธ ๊ฐ™์€ ์ด๋ฉ”์ผ์ด ์žˆ์œผ๋ฉด ๊ธฐ์กดํšŒ์›์œผ๋กœ ๋กœ๊ทธ์ธ
            User googleUser = registerGoogleOrUpdateGoogle(snsUserInfoDto);
    
            // 4. ๊ฐ•์ œ ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ
            final String AUTH_HEADER = "Authorization";
            final String TOKEN_TYPE = "BEARER";
    
            String jwt_token = forceLogin(googleUser); // ๋กœ๊ทธ์ธ์ฒ˜๋ฆฌ ํ›„ ํ† ํฐ ๋ฐ›์•„์˜ค๊ธฐ
            HttpHeaders headers = new HttpHeaders();
            headers.set(AUTH_HEADER, TOKEN_TYPE + " " + jwt_token);
            GoogleUserResponseDto googleUserResponseDto = GoogleUserResponseDto.builder()
                    .token(TOKEN_TYPE + " " + jwt_token)
                    .username(googleUser.getUsername())
                    .build();
            System.out.println("Google user's token : " + TOKEN_TYPE + " " + jwt_token);
            System.out.println("LOGIN SUCCESS!");
            return googleUserResponseDto;
        }
    
        private String getAccessToken(RestTemplate restTemplate, String code) throws JsonProcessingException {
    
            //Google OAuth Access Token ์š”์ฒญ์„ ์œ„ํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ธํŒ…
            GoogleOAuthRequest googleOAuthRequestParam = GoogleOAuthRequest
                    .builder()
                    .clientId(clientId)
                    .clientSecret(clientSecret)
                    .code(code)
                    //.redirectUri("https://memegle.xyz/redirect/google")
                    //.redirectUri("http://localhost:3000/redirect/google")
                    .redirectUri("http://localhost:8080/api/user/google/callback")
                    .grantType("authorization_code").build();
    
            //JSON ํŒŒ์‹ฑ์„ ์œ„ํ•œ ๊ธฐ๋ณธ๊ฐ’ ์„ธํŒ…
            //์š”์ฒญ์‹œ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ์Šค๋„ค์ดํฌ ์ผ€์ด์Šค๋กœ ์„ธํŒ…๋˜๋ฏ€๋กœ Object mapper์— ๋ฏธ๋ฆฌ ์„ค์ •ํ•ด์ค€๋‹ค.
            ObjectMapper mapper = new ObjectMapper();
            mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
            mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    
            //AccessToken ๋ฐœ๊ธ‰ ์š”์ฒญ
            ResponseEntity<String> resultEntity = restTemplate.postForEntity("https://oauth2.googleapis.com/token", googleOAuthRequestParam, String.class);
    
            //Token Request
            GoogleOAuthResponse result = mapper.readValue(resultEntity.getBody(), new TypeReference<GoogleOAuthResponse>() {
            });
    
            String jwtToken = result.getId_token();
    
            return jwtToken;
        }
    
        private GoogleUserInfoDto getGoogleUserInfo(RestTemplate restTemplate, String jwtToken) throws JsonProcessingException {
            ObjectMapper mapper = new ObjectMapper();
            String requestUrl = UriComponentsBuilder.fromHttpUrl("https://oauth2.googleapis.com/tokeninfo")
                    .queryParam("id_token", jwtToken).encode().toUriString();
    
            String resultJson = restTemplate.getForObject(requestUrl, String.class);
    
            Map<String,String> userInfo = mapper.readValue(resultJson, new TypeReference<Map<String, String>>(){});
    
            
            GoogleUserInfoDto googleUserInfoDto = GoogleUserInfoDto.builder()
                    .username(userInfo.get("email"))
                    .nickname(userInfo.get("name"))
                    .profileImage(userInfo.get("picture"))
                    .build();
    
            return googleUserInfoDto;
        }
    
        private User registerGoogleOrUpdateGoogle(GoogleUserInfoDto googleUserInfoDto) {
            User sameUser = userRepository.findUserByUsername(googleUserInfoDto.getUsername());
    
            if (sameUser == null) {
                return registerGoogleUserIfNeeded(googleUserInfoDto);
            }
            else {
               return updateGoogleUser(sameUser, googleUserInfoDto);
            }
    
        }
    
        private User registerGoogleUserIfNeeded(GoogleUserInfoDto googleUserInfoDto) {
    
            // DB ์— ์ค‘๋ณต๋œ google Id ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ
            String googleUserId = googleUserInfoDto.getUsername();
    
            User googleUser = userRepository.findUserByUsername(googleUserId);
    
            if (googleUser == null) {
                // ํšŒ์›๊ฐ€์ž…
                // username: google ID(email)
                String username = googleUserInfoDto.getUsername();
    
                // profileImage: google profile image
                String profileImage = googleUserInfoDto.getProfileImage();
    
                // password: random UUID
                String password = UUID.randomUUID().toString();
                String encodedPassword = passwordEncoder.encode(password);
    
                googleUser = User.builder()
                        .username(username)
                        .password(encodedPassword)
                        .build();
                userRepository.save(googleUser);
            }
    
            return googleUser;
        }
    
        private User updateGoogleUser(User sameUser, GoogleUserInfoDto googleUserInfoDto) {
            if (sameUser.getUsername() == null) {
                System.out.println("์ค‘๋ณต");
                sameUser.setUsername(googleUserInfoDto.getUsername());
                userRepository.save(sameUser);
            }
            return sameUser;
        }
    
        private String forceLogin(User googleUser) {
            UserDetailsImpl userDetails = new UserDetailsImpl(googleUser);
            Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
    
            return JwtTokenUtils.generateJwtToken(userDetails);
        }
    }

Optional ๊ฐ์ฒด

  • Before

    public interface UserRepository extends JpaRepository<User, Long> {
        Optional<User> findByUsername(String username);
    public class UserDetailsServiceImpl implements UserDetailsService {
    ~
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    				Optional<User> user = userRepository.findByUsername(username);
    				return new UserDetailsImpl(user.get());
    public class JWTAuthProvider implements AuthenticationProvider {
    		@Override
        public Authentication authenticate(Authentication authentication)
    				Optional<User> user = userRepository.findUserByUsername(username).get();
    java.util.NoSuchElementException: No value present
    	at java.base/java.util.Optional.get(Optional.java:143) ~[na:na]
  • After

    public interface UserRepository extends JpaRepository<User, Long> {
        User findByUsername(String username);
    public class UserDetailsServiceImpl implements UserDetailsService {
    ~
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userRepository.findUserByUsername(username);
            return new UserDetailsImpl(user);
    public class JWTAuthProvider implements AuthenticationProvider {
    		@Override
        public Authentication authenticate(Authentication authentication)
    				User user = userRepository.findUserByUsername(username);
  • Why

    public class FormLoginAuthProvider implements AuthenticationProvider {
    		@Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    				UserDetailsImpl userDetails = (UserDetailsImpl) userDetailsService.loadUserByUsername(username);
    		        if (userDetails.getUser() == null){
    		            throw new UsernameNotFoundException("์ด๋ฉ”์ผ ์ฃผ์†Œ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
    		        }

    Optional<User> ๋ณ€์ˆ˜๋ฅผ ์„ ์–ธ ํ›„ .get() ๋ฉ”์†Œ๋“œ๋กœ ๊ฐ€์ ธ์˜ฌ ๋•Œ Optional์ด ๋น„์–ด์žˆ์„ ๋•Œ NoSuchElementException์ด ๋ฐ˜ํ™˜๋œ๋‹ค. Optional์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ˜ํ™˜๋œ ๊ฐ’์€ null์ด ์•„๋‹Œ ๊ณต๋ž€๊ฐ’์ด๋ฉฐ ๊ณต๋ž€๊ฐ’์„ .get()ํ•˜๊ธฐ์— java.util.NoSuchElementException: No value present์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ด๋‹ค.


๐Ÿ“ย 9. API

๊ธฐ๋Šฅ MTHD URL request response ๋น„๊ณ 
ํšŒ์›๊ฐ€์ž…, signup POST /api/signup {
"username": "test1231246",
"password": "test",
"passwordCheck": "test",
"businessPart": "test",
"job": "test"
}
{
โ€™okโ€™: true,
message: โ€˜ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณตโ€™
}
OR
{
โ€˜okโ€™: false,
message:โ€™ํšŒ์›๊ฐ€์ž… ์‹คํŒจโ€™
}
๋กœ๊ทธ์ธ, login POST /api/login {
โ€usernameโ€: โ€usernameโ€,
โ€passwordโ€: โ€passwordโ€,
}
{
โ€™okโ€™: true,
message: โ€˜๋กœ๊ทธ์ธ ์„ฑ๊ณตโ€™
}
OR
{
โ€˜okโ€™: false,
message:โ€™๋กœ๊ทธ์ธ ์‹คํŒจโ€™
}
ํ™ˆํŽ˜์ด์ง€ ์กฐํšŒ GET / [
{
โ€project_idโ€: Long(int),
โ€imageUrlโ€
โ€titleโ€:โ€titleโ€,
"budget":
โ€descriptionโ€:,
"workingPeriod":
โ€total_lenโ€: int
},
] 4๊ฐœ ์ œํ•œ
ํ”„๋กœ์ ํŠธ ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€ ๋ชจ๋‘ ์กฐํšŒ GET /projects {
"page": (int) page,
"size": (int) size,
"sortByโ€ : (String)"createAt" or "budget"or "volunteerValidDate"
}
[
{
โ€project_idโ€: Long(int),
โ€imageUrlโ€:,
"progressMethod":,
โ€leftDaysForEndโ€:โ€volunteerValidDateโ€-โ€todayDateโ€,
โ€titleโ€:โ€titleโ€,
"budget":
"bigCategory":
"smallCategory":
โ€descriptionโ€:,
"WorkingPeriod":,
โ€taxInvoiceโ€:,
โ€progressmethodโ€
},
]
๊ธฐ๋ณธ๊ฐ’โ†’์ตœ์‹ ๋“ฑ๋ก์ˆœ
๋งˆ์ดํŽ˜์ด์ง€ ํ”„๋กœ์ ํŠธ ์กฐํšŒ GET /mypage/projects [
{
โ€project_idโ€: Long(int),
โ€imageUrlโ€:โ€์„œ๋ฒ„ ๋‚ด๋ถ€ ์ €์žฅ๋œ ์‚ฌ์ง„โ€,
โ€titleโ€:โ€titleโ€,
"budget":"budget",
"bigCategory":"bigCategory",
"smallCategory":"smallCategory"
}
]
๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ POST /projects/project header : token
{
"bigCategory":"[string] ์ƒ์œ„ ์นดํ…Œ๊ณ ๋ฆฌ",
"smallCategory":"[string] ํ•˜์œ„ ์นดํ…Œ๊ณ ๋ฆฌ",
"progressMethod":"(ํ”„๋กœ์ ํŠธ ์ง„ํ–‰ ๋ฐฉ์‹)์™ธ์ฃผ",
"projectScope":"(500๋งŒ์› ๋ฏธ๋งŒ)",
"title": "[string] ์˜๋ขฐ์„œ๋น„์Šค์˜ ์ œ๋ชฉ",
โ€descriptionโ€:"[string] ์„ค๋ช…",
"currentStatus":"[string] ํ”„๋กœ์ ํŠธ ์ค€๋น„์ƒํ™ฉ",
"requiredFunction":"[string] ๊ธฐ๋ณธ๊ธฐ๋Šฅ",
"userRelatedFunction":"[string] ํšŒ์› ๊ด€๋ จ ๊ธฐ๋Šฅ",
"commerceRelatedFunction":"[string] ์ปค๋จธ์Šค ๊ด€๋ จ ๊ธฐ๋Šฅ",
"siteEnvironment":"[string] ์‚ฌ์ดํŠธ ํ™˜๊ฒฝ",
"solutionInUse":"df",
"reactable":"[string] ๋ฐ˜์‘ํ˜• ์ ์šฉ ์—ฌ๋ถ€",
"budget":100000,
"taxInvoice":"true",
"volunteerValidDate": "dflfke",
"dueDateForApplication":"dfdfe",
"workingPeriod":30
},
โ€filesโ€:[{
โ€fileUrlโ€:โ€fileUrlโ€,
โ€fileNameโ€:โ€fileNameโ€
},]
}
{
โ€™okโ€™: true,
message: โ€˜๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ • ์™„๋ฃŒโ€™
}
OR
{
โ€˜okโ€™: false,
message:โ€™๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ • ์™„๋ฃŒโ€™
}
์š”์ฒญ ๊ธฐ๋Šฅ์€ ํ•œ๊ธ€ ์ŠคํŠธ๋ง ๊ทธ๋Œ€๋กœ ๋ณด๋‚ด์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.
๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ GET /projects/{projectId} "progressMethod":"(ํ”„๋กœ์ ํŠธ ์ง„ํ–‰ ๋ฐฉ์‹)์™ธ์ฃผ",
"projectScope":"(500๋งŒ์› ๋ฏธ๋งŒ)",
"title": "[string] ์˜๋ขฐ์„œ๋น„์Šค์˜ ์ œ๋ชฉ",
โ€descriptionโ€:"[string] ์„ค๋ช…",
"currentStatus":"[string] ํ”„๋กœ์ ํŠธ ์ค€๋น„์ƒํ™ฉ",
"requiredFunction":"[string] ๊ธฐ๋ณธ๊ธฐ๋Šฅ",
"userRelatedFunction":"[string] ํšŒ์› ๊ด€๋ จ ๊ธฐ๋Šฅ",
"commerceRelatedFunction":"[string] ์ปค๋จธ์Šค ๊ด€๋ จ ๊ธฐ๋Šฅ",
"siteEnvironment":"[string] ์‚ฌ์ดํŠธ ํ™˜๊ฒฝ",
"solutionInUse":"df",
"reactable":"[string] ๋ฐ˜์‘ํ˜• ์ ์šฉ ์—ฌ๋ถ€",
"budget":100000,
"taxInvoice":"true",
"volunteerValidDate": "dflfke",
"dueDateForApplication":"dfdfe",
"workingPeriod":30
},
โ€filesโ€:[{
โ€fileUrlโ€:โ€fileUrlโ€,
โ€fileNameโ€:โ€fileNameโ€
},]
}
์š”์ฒญ ๊ธฐ๋Šฅ์€ ํ•œ๊ธ€ ์ŠคํŠธ๋ง ๊ทธ๋Œ€๋กœ ๋ณด๋‚ด์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.
๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ • PUT /projects/project/{projectId} header : token
{
"bigCategory":"[string] ์ƒ์œ„ ์นดํ…Œ๊ณ ๋ฆฌ",
"smallCategory":"[string] ํ•˜์œ„ ์นดํ…Œ๊ณ ๋ฆฌ",
"progressMethod":"(ํ”„๋กœ์ ํŠธ ์ง„ํ–‰ ๋ฐฉ์‹)์™ธ์ฃผ",
"projectScope":"(500๋งŒ์› ๋ฏธ๋งŒ)",
"title": "[string] ์˜๋ขฐ์„œ๋น„์Šค์˜ ์ œ๋ชฉ",
โ€descriptionโ€:"[string] ์„ค๋ช…",
"currentStatus":"[string] ํ”„๋กœ์ ํŠธ ์ค€๋น„์ƒํ™ฉ",
"requiredFunction":"[string] ๊ธฐ๋ณธ๊ธฐ๋Šฅ",
"userRelatedFunction":"[string] ํšŒ์› ๊ด€๋ จ ๊ธฐ๋Šฅ",
"commerceRelatedFunction":"[string] ์ปค๋จธ์Šค ๊ด€๋ จ ๊ธฐ๋Šฅ",
"siteEnvironment":"[string] ์‚ฌ์ดํŠธ ํ™˜๊ฒฝ",
"solutionInUse":"df",
"reactable":"[string] ๋ฐ˜์‘ํ˜• ์ ์šฉ ์—ฌ๋ถ€",
"budget":100000,
"taxInvoice":"true",
"volunteerValidDate": "dflfke",
"dueDateForApplication":"dfdfe",
"workingPeriod":30
}
{
"bigCategory":"[string] ์ƒ์œ„ ์นดํ…Œ๊ณ ๋ฆฌ",
"smallCategory":"[string] ํ•˜์œ„ ์นดํ…Œ๊ณ ๋ฆฌ",
"progressMethod":"(ํ”„๋กœ์ ํŠธ ์ง„ํ–‰ ๋ฐฉ์‹)์™ธ์ฃผ",
"projectScope":"(500๋งŒ์› ๋ฏธ๋งŒ)",
"title": "[string] ์˜๋ขฐ์„œ๋น„์Šค์˜ ์ œ๋ชฉ",
โ€descriptionโ€:"[string] ์„ค๋ช…",
"currentStatus":"[string] ํ”„๋กœ์ ํŠธ ์ค€๋น„์ƒํ™ฉ",
"requiredFunction":"[string] ๊ธฐ๋ณธ๊ธฐ๋Šฅ",
"userRelatedFunction":"[string] ํšŒ์› ๊ด€๋ จ ๊ธฐ๋Šฅ",
"commerceRelatedFunction":"[string] ์ปค๋จธ์Šค ๊ด€๋ จ ๊ธฐ๋Šฅ",
"siteEnvironment":"[string] ์‚ฌ์ดํŠธ ํ™˜๊ฒฝ",
"solutionInUse":"df",
"reactable":"[string] ๋ฐ˜์‘ํ˜• ์ ์šฉ ์—ฌ๋ถ€",
"budget":100000,
"taxInvoice":"true",
"volunteerValidDate": "dflfke",
"dueDateForApplication":"dfdfe",
"workingPeriod":30
},
โ€filesโ€:[{
โ€fileUrlโ€:โ€fileUrlโ€,
โ€fileNameโ€:โ€fileNameโ€
},]
}
๋งˆ์ดํŽ˜์ด์ง€ ํŽธ์ง‘ ์™„๋ฃŒ๋ฅผ ๋ˆŒ๋ €์„ ๋•Œ ์š”์ฒญํ•ด์•ผ ํ•  API
๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ • ์ „ ์กฐํšŒ GET /modal/{projectId} token {
"bigCategory":"[string] ์ƒ์œ„ ์นดํ…Œ๊ณ ๋ฆฌ",
"smallCategory":"[string] ํ•˜์œ„ ์นดํ…Œ๊ณ ๋ฆฌ",
"progressMethod":"(ํ”„๋กœ์ ํŠธ ์ง„ํ–‰ ๋ฐฉ์‹)์™ธ์ฃผ",
"projectScope":"(500๋งŒ์› ๋ฏธ๋งŒ)",
"title": "[string] ์˜๋ขฐ์„œ๋น„์Šค์˜ ์ œ๋ชฉ",
โ€descriptionโ€:"[string] ์„ค๋ช…",
"currentStatus":"[string] ํ”„๋กœ์ ํŠธ ์ค€๋น„์ƒํ™ฉ",
"requiredFunction":"[string] ๊ธฐ๋ณธ๊ธฐ๋Šฅ",
"userRelatedFunction":"[string] ํšŒ์› ๊ด€๋ จ ๊ธฐ๋Šฅ",
"commerceRelatedFunction":"[string] ์ปค๋จธ์Šค ๊ด€๋ จ ๊ธฐ๋Šฅ",
"siteEnvironment":"[string] ์‚ฌ์ดํŠธ ํ™˜๊ฒฝ",
"solutionInUse":"df",
"reactable":"[string] ๋ฐ˜์‘ํ˜• ์ ์šฉ ์—ฌ๋ถ€",
"budget":100000,
"taxInvoice":"true",
"volunteerValidDate": "dflfke",
"dueDateForApplication":"dfdfe",
"workingPeriod":30
}
๋งˆ์ดํŽ˜์ด์ง€์—์„œ ํŽธ์ง‘ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ์˜ ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ
๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ DELET /projects/project/{projectId} header: token {
โ€okโ€ : true,
message : ์‚ญ์ œ ์™„๋ฃŒ
}
์ „์ฒด ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ GET /projects [
{
โ€์„œ๋ฒ„ ๋‚ด๋ถ€ ์ €์žฅ๋œ ์‚ฌ์ง„โ€
โ€leftDaysForEndโ€:โ€volunteerValidDateโ€-โ€todayDateโ€(์ž๋ฐ” ํ˜„์žฌ ์‹œ๊ฐ„),
โ€titleโ€:โ€titleโ€,
"budget":
"bigCategory":
"smallCategory":
โ€descriptionโ€:,
"WorkingPeriod":
},
]
ํŒŒ์ผ ์ „์†ก POST /projects/project/file {
ํผ๋ฐ์ดํ„ฐ๋กœ ํŒŒ์ผ ๋ณด๋‚ด๊ธฐ
}
{
โ€™okโ€™: true,
message: โ€˜ํŒŒ์ผ ์—…๋กœ๋“œ ์™„๋ฃŒโ€™
}
OR
{
โ€˜okโ€™: false,
message:โ€™ํŒŒ์ผ ์—…๋กœ๋“œ ์‹คํŒจโ€™
}
ํŒŒ์ผ ์‚ญ์ œ DELETE /projects/project/file/{projectId} {
โ€™okโ€™: true,
message: โ€˜ํŒŒ์ผ ์‚ญ์ œ์™„๋ฃŒโ€™
}
OR
{
โ€˜okโ€™: false,
message:โ€™ํŒŒ์ผ ์‚ญ์ œ์‹คํŒจโ€™
}

๐Ÿ’ย 10์กฐ ํ•œ ์ค„ ํšŒ๊ณ 

๋ˆ„๊ตฐ๊ฐ€์˜ ๋’ท๋ชจ์Šต์ด ๋ณด์ด๊ธฐ ์‹œ์ž‘ํ•˜๋Š” ๊ฒƒ์€ ์‚ฌ๋ž‘ ๋•Œ๋ฌธ๋งŒ์ด ์•„๋‹ˆ๋ผ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ๋Š” ๊ฒƒ์„ ๋ฐฐ์šด ํ•œ ์ฃผ์˜€์Šต๋‹ˆ๋‹ค. ์ง€๋‚œ ์‚ผ ์ฃผ ๋™์•ˆ ์Šคํ”„๋ง์„ ๊ณต๋ถ€ํ•˜๋ฉด์„œ โ€œ์•„ ๋‚ด๊ฐ€ ์™œ ์ด๋ ‡๊ฒŒ ์–ด๋ ค์šด ์Šคํ”„๋ง์„ ์„ ํƒํ•ด์„œ ์ด๋ ‡๊ฒŒ ๊ณ ์ƒํ•˜๋Š” ๊ฒƒ์ผ๊นŒ?โ€๋ž€ ์ƒ๊ฐ์ด ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ์ž๋ฐ”์™€ ์Šคํ”„๋ง์€ ๊ฐ€์‹œ์ ์ด์ง€ ์•Š์„ ๋ฟ๋”๋Ÿฌ MVC๊ณผ ์—ญํ• ๊ณผ ์ฑ…์ž„์„ ๋ถ„ํ• ํ•˜๋Š” ์ˆ˜ ๋งŽ์€ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด์•ผํ•˜๊ณ , ๋ณด์•ˆ์€ ์„ฃ๋ถˆ๋ฆฌ ์†์„ ๋Œ€๊ธฐ ์–ด๋ ค์šด ์ˆ˜์ค€์ด์—ˆ์œผ๋‹ˆ๊นŒ์š”.

ํ•˜์ง€๋งŒ ๋ฆฌ์•กํŠธ์™€ ๊ฐ™์ด ํ˜‘์—…์„ ํ•˜๊ณ ๋‚˜๋‹ˆ ์•Œ์•˜์Šต๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ๊ฐ€ ์Šคํ”„๋ง๋ณด๋‹ค ํ›จ์”ฌ ๋” ๋งŽ์€ ์‹œ๊ฐ„๊ณผ ๋…ธ๋ ฅ์„ ์Ÿ์•„์•ผ ํ”„๋กœ์ ํŠธ๊ฐ€ ๋๋‚  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ๋ง์ด์—์š”. ์Šคํ”„๋ง์ด ๊ตฌ์กฐ ์„ค๊ณ„ํ•˜๊ณ  ์—ฌ์œ ๊ฐ€ ์žˆ์„ ๋•Œ๋„ ๋ฆฌ์•กํŠธ์—์„œ๋Š” ๋จธ๋ฆฌ๋ฅผ ์ฅ์–ด์งœ๊ณ  ์ฝ”๋“œ๋ฅผ ์งœ๊ณ  ์žˆ๋Š”๊ฑธ ๋ณด๋‹ˆ ๊ทธ๋“ค์˜ ๋“ฑ์ด ๋ณด์ด๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฐฑ์—”๋“œ์˜ ๊ตฌ์กฐ ์„ค๊ณ„๋Š” ๋์ด ์žˆ์ง€๋งŒ ์‹ฌ๋ฏธ์  ์š”์†Œ๊ฐ€ ๊ฐ๋ฏธ๋œ ํ”„๋ก ํŠธ์—์„œ๋Š” ๊ทธ ๋์„ ์ •ํ•ด์•ผ ํ•˜๋‹ˆ๊นŒ์š”.

ํ•œ ์ฃผ ๋™์•ˆ ์ž ์„ ์•„๊ปด๊ฐ€๋ฉฐ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งˆ๋ฌด๋ฆฌ ํ•ด์ฃผ์…จ๋˜ ๋ชจ๋“  ๋ถ„๋“ค์—๊ฒŒ ๊ฐ์‚ฌ ์ธ์‚ฌ๋ฅผ ๋Œ๋ฆฝ๋‹ˆ๋‹ค. ํ•ญํ•ด99์˜ ๋‚จ์€ ๊ธฐ๊ฐ„ ๋™์•ˆ ์ŠคํŠธ๋ ˆ์Šค ์—†์ด ์›ํ•˜์‹œ๋Š” ๊ฒฐ๊ณผ ์–ป์œผ์‹œ๊ธธ ๊ธฐ๋„ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. - ์ด๋™์žฌ

ํด๋ก  ์ฝ”๋”ฉ 1์ฃผ์ผ ๋™์•ˆ ๋งŽ์€ ๊ฒƒ๋“ค์„ ๋ฐฐ์› ๋˜ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์ €๋ฒˆ ์ฃผ์ฐจ๊ฐ€ ํ”„๋ก ํŠธ์™€ ๋ฐฑ ๊ฐ„์˜ ํ˜‘์—…์˜ ๊ฐœ๋…์„ ์ค‘์‹ฌ์œผ๋กœ ์ƒ๊ฐํ•˜๋Š” ๊ด€์ ์„ ๊ธธ๋Ÿฌ์ฃผ๋Š” ์ฃผ์ฐจ์˜€๋‹ค๋ฉด ์ด๋ฒˆ ์ฃผ์ฐจ๋Š” ์‹ค์ œ ์„œ๋น„์Šค ์ค‘์ธ ์‚ฌ์ดํŠธ๋“ค์ด ์–ด๋–ค ๋ฐฉํ–ฅ์œผ๋กœ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๊ตฌํ˜„๋˜์–ด ์žˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ๋Š” ์ฃผ์ฐจ์˜€์Šต๋‹ˆ๋‹ค. ์Šคํ”„๋ง์—์„œ ํŠน์ • ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ์–ด๋– ํ•œ ํ•จ์ˆ˜๋ฅผ ์จ์•ผ ํ•˜๋Š”๊ฐ€, ์›น์†Œ์ผ“, ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ, S3 ๋“ฑ๋“ฑ ๋ชฉ์ ์— ๋งž๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๋Šฅ ํ•จ์ˆ˜๋“ค์€ ์–ด๋– ํ•œ ๊ฒƒ์ธ๊ฐ€ ์ด๋Ÿฐ ๊ณ ๋ฏผ์„ ๋งŽ์ด ํ•˜๊ฒŒ ๋œ ์ฃผ๊ฐ„์ด์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฌผ๋ก  ์‚ฌ์ •์ƒ ํ”„๋ก ํŠธ์™€ ๋ฐฑ ๊ฐ„์˜ ํ˜‘์˜ ํ•˜์— ๊ตฌํ˜„ํ•˜์ง€ ๋ชปํ•œ ๊ธฐ๋Šฅ๋“ค์ด ๋งŽ์•˜์ง€๋งŒ ์ถ”ํ›„ ์‹ค์ „ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์‹œ๊ฐ„์„ ๊ธธ๊ฒŒ ์žก๊ณ  ๊ตฌํ˜„ํ•˜์ง€ ๋ชปํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์ถ”๊ฐ€ํ•ด ํด๋ก  ์ฝ”๋”ฉ์—์„œ ์‹ค์ œ ์„œ๋น„์Šค์™€ ํ˜„์žฌ ๋‚˜์˜ ์‹ค๋ ฅ ๊ฐ„์— ์กด์žฌํ•˜๋˜ ๊ดด๋ฆฌ๋ฅผ ์ขํžˆ๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ๋‚˜ ๊ณ ์ƒ์ด ๋งŽ์œผ์…จ๋˜ ํ”„๋ก ํŠธ ๋ถ„๋“ค์—๊ฒŒ ์‘์›์˜ ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋‚ด๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. - ๋ฐ•์„ธ์—ด

์‹ค์ „ ํ”„๋กœ์ ํŠธ ์ „ ๋งˆ์ง€๋ง‰ ํŒ€ ํ”„๋กœ์ ํŠธ์ด๋‹ค ๋ณด๋‹ˆ ์•„์‰ฌ์›€์ด ๊ฝค๋‚˜ ๋‚จ๋Š”๋‹ค. ์•Œ๊ณ ๋Š” ์žˆ์—ˆ์ง€๋งŒ API ์„ค๊ณ„์˜ ์ค‘์š”์„ฑ์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ๋А๊ผˆ๋‹ค. ๊ธ‰ํ•˜๊ฒŒ API ๋ฅผ ์„ค๊ณ„ํ•ด์„œ์ธ์ง€ ์ฝ”๋“œ๋ฅผ ์งœ๋Š” ์ค‘๊ฐ„์ค‘๊ฐ„ ํ‹ˆ์ด ๋ณด์˜€๊ณ  ๊ทธ ํ‹ˆ์„ ๊ธ‰ํ•˜๊ฒŒ ๋ฉ”๊ฟ”๊ฐ€๋ฉฐ ์ฝ”๋“œ๋ฅผ ์งœ๋‹ค ๋ณด๋‹ˆ ์ข€ ๋” ํšจ์œจ์ ์ด๊ณ  ๋ณด๊ธฐ ์ข‹์€ ์ฝ”๋“œ์— ๋Œ€ํ•œ ๊ณ ๋ฏผ์ด ๋ถ€์กฑํ–ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค. CRUD ๊ธฐ๋Šฅ์„ ๋งก๊ฒŒ ๋˜๋ฉด์„œ CRUD ์— ๋” ์ต์ˆ™ํ•ด์ง€๋Š” ์‹œ๊ฐ„์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์—ˆ๊ณ , ํ”„๋ก ํŠธ ๋ถ„๋“ค์ด ์–ด๋–ค ๋ถ€๋ถ„์ด ์™œ ํ•„์š”ํ•œ์ง€ ์ž์„ธํžˆ ์„ค๋ช…ํ•ด์ฃผ์…”์„œ ์–ด๋–ค ์žฌ๋ฃŒ๋ฅผ ์ฃผ๋Š” ๊ฒŒ ๋” ํŽธํ• ์ง€์— ๋Œ€ํ•œ ๊ฐ์ด ์žกํ˜”๋‹ค. ํ•œ ์ฃผ ๋™์•ˆ ํ•จ๊ป˜ ํž˜๋‚ด์ฃผ์…จ๋˜ 8์กฐ ํŒ€์›๋“ค์—๊ฒŒ ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค. - ๊น€๋ฏผ์ง€

์งง์•˜๋˜ ํด๋ก ์ฝ”๋”ฉ์ด์ง€๋งŒ, ์—ฌ๋Ÿฌ CSS ์†์„ฑ์„ ์•Œ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์‹œ๊ฐ„์ด ์งง์•„ ๋งŽ์€ ๋ถ€๋ถ„์„ ๊ณต๋ถ€ํ•˜์ง„ ๋ชปํ–ˆ์ง€๋งŒ ๊ธฐํšŒ๊ฐ€ ๋œ๋‹ค๋ฉด ์ข€ ๋” ๋‹ค๋“ฌ์„ ์ˆ˜ ์žˆ๋Š” ์‹œ๊ฐ„์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์œผ๋ฉด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ์— ๊ธฐ๋Šฅ ๊ตฌํ˜„์ด ๋งŽ์ด ๋Šฆ์–ด์ ธ์„œ ๋งŽ์€ ๊ธฐ๋Šฅ์„ ๋‹ด์„ ์ˆ˜ ์—†์—ˆ๋˜ ๋ถ€๋ถ„์— ๋Œ€ํ•ด ๋ฐฑ์—”๋“œ ๋ถ„๋“ค๊ป˜ ์‚ฌ๊ณผ์˜ ๋ง์”€ ๋“œ๋ฆฌ๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. - ํ•œ์ง€์šฉ

ํด๋ก  ํ”„๋กœ์ ํŠธ๋ฅผ ํ•˜๋ฉด์„œ ์‹ค์ œ ์‚ฌ์ดํŠธ๊ฐ€ ์–ผ๋งˆ๋‚˜ ๊ผผ๊ผผํ•˜๊ณ  ์„ธ๋ฐ€ํ•˜๊ฒŒ ์„ค๊ณ„๊ฐ€ ๋˜์—ˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ํ‰์†Œ์—๋Š” ๊ทธ์ € ์ด์šฉํ•˜๊ธฐ๋งŒ ํ–ˆ๋˜ ์‚ฌ์ดํŠธ๋ฅผ ํด๋ก ํ•˜๊ธฐ ์œ„ํ•ด ๊ตฌ์กฐ๋ฅผ ์‚ดํ”ผ๊ณ  ๋งŒ๋“œ๋Š” ๊ณผ์ •์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ๋ฆฌํ•˜๊ฒŒ ์‚ฌ์šฉํ• ์ˆ˜๋ก ๋’ค์—์„œ ๊ฐœ๋ฐœ์ž๋“ค์€ ๋” ์ผ์„ ๋งŽ์ด ํ•ด์•ผํ•˜๋Š” ๊ฒƒ์ด๊ตฌ๋‚˜! ๋ฅผ ๊นจ๋‹ฌ์•˜์Šต๋‹ˆ๋‹ค. ์ €ํฌ๊ฐ€ ์•ž์—์„œ ์ž‘์—…ํ•  ๋•Œ ํŽธํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์„œ ์“ธ ์ˆ˜ ์žˆ๋„๋ก ๊ฐ™์ด ์—ด์‹ฌํžˆ ํ•˜์‹  ๋ฐฑ์—”๋“œ ๋ถ„๋“ค๋„ ๋ชจ๋‘ ์ˆ˜๊ณ ํ•˜์…จ์Šต๋‹ˆ๋‹ค! -์ด๊ฐ€์—ฐ

์‹ค์ „ ํ”„๋กœ์ ํŠธ ์ „ ๊ธฐ๋ณธ CRUD ๊ธฐ๋Šฅ์— ๋Œ€ํ•ด ์™„๋ฒฝํ•˜๊ฒŒ ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ์—ˆ๋˜ ์‹œ๊ฐ„์ด์—ˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ ์„œ๋น„์Šค๊ฐ€ ํŽ˜์ด์ง€์— ๊ทธ๋ ค์ง€๋Š” ๊ฒƒ์„ ์ž ์‹œ๋‚˜๋งˆ ๊ฒฝํ—˜ํ•ด๋ณด๋‹ˆ ํ”„๋ก ํŠธ ๊ฐœ๋ฐœ์ž๋กœ์„œ UX ์ ์œผ๋กœ ์–ผ๋งˆ๋‚˜ ๋””ํ…Œ์ผํ•˜๊ณ  ์น˜๋ฐ€ํ•˜๊ฒŒ ์ ‘๊ทผํ•ด์•ผํ•˜๋Š” ์ง€๋ฅผ ์•Œ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋’ค์—์„œ ๋“ ๋“ ํ•˜๊ฒŒ ๋ฐ›์ณ์ฃผ์‹œ๋Š” ๋ฐฑ ๋ถ„๋“ค ๋•๋ถ„์— ๋งˆ์Œ ํŽธํžˆ ํ”„๋ก ํŠธ๋“ค์€ ์•ž๋งŒ ๋ณด๊ณ  ๋‹ฌ๋ฆด ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ํ•œ ์ฃผ๋™์•ˆ ๋„ˆ๋ฌด ๊ณ ์ƒ๋งŽ์œผ์…จ๊ณ  ๊ฐ์‚ฌํ–ˆ์Šต๋‹ˆ๋‹ค! ๋‚จ์€ ํ•ญํ•ด ์‹œ๊ฐ„๋„ ํฌ๋ชฝ์ด๋“ค ์ˆœํ•ญํ•˜์„ธ์š”๐Ÿ›ณ - ์กฐํ•ด์†”

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Java 100.0%