์ ์ ๋๋ฆฌ๋ธ(User driven)ํ๋ฉด์ ํ๋ก๋ฐ์ด๋ ๋๋ฆฌ๋ธ(provider driven)ํ ์นํ์ด์ง๋ฅผ ์ ์ํ๊ณ ์ ํฌ๋ชฝ์ ์ ํํ๊ฒ ๋์์ต๋๋ค. ์ด๋ ๊ฐ๋ฐ์์ ๊ธฐ๋ณธ์ ์ธ ์ญ๋์ ํค์ฐ๋ ค๋ ๊ฒ๊ณผ ๋์์ ์๋ก์ด ๊ธฐ๋ฅ๊ตฌํ์ ํ๋ ๊ฒ์ ๋ชฉํํ ๊ฒฐ๊ณผ์ ๋๋ค.
๋ถํธ์บ ํ ํญํด99์ ์ฒ์ ํ ๋ฌ์ด ์กฐ๊ธ ๋์ ์๊ฐ๋์ ์๊ฐ์๋ค์ ํ์๊ฐ์ , ๋ก๊ทธ์ธ ๊ทธ๋ฆฌ๊ณ CRUD ๊ธฐ๋ฅ์ ์ค์ ์ ์ผ๋ก ๋ค๋ฃน๋๋ค. ๊ทธ๋ ๊ธฐ์ ํด๋ก ์ ๋ํ ๋ชฉํ๋ก ์ธ์คํ๊ทธ๋จ, ์ฌ๋, ๋น๊ทผ๋ง์ผ๊ณผ ๊ฐ์ ์ ์ ์ ํ๋ก๋ฐ์ด๋๊ฐ ์ํธ์์ฉํ๋ ์๋น์ค๋ฅผ ํด๋ก ํ๋ ๊ฒฝํฅ์ด ์๊น๋๋ค. ์ด์ ๋ํ ์ฐธ๊ณ ์๋ฃ๋ก์ ์์ ํญํด99 ์๊ฐ์๋ค์ ์ํ์ ํ์ธํ ์ ์์์ต๋๋ค.
ํ์๋ค๊ณผ ๊ธฐ๋๊ธด ๋ ผ์๋ฅผ ๋์ผ๋ก ์ ํฌ ํ์ ์ด์ ์๊ฐ์๋ค์ด ํ์ง ์์ ํด๋ก ์ ๋ชฉ์ ์ผ๋ก โํฌ๋ชฝ'์ ์ ํํ์ต๋๋ค. ๋์์์ ๊ฑด๋๋ ์ฝ๋กฌ๋ฒ์ค์ ๋ง์์ ๊ฐ์ ธ๋ณด๊ธฐ๋ก ํ ๊ฒ์ ๋๋ค. ์ฐธ๊ณ ์๋ฃ ๋ง์ง ์๊ธฐ์ ๋ง๋งํ์ง๋ง ์์ ์ฑ๊ณต์ ๊ฟ๊พธ๊ธฐ์ ํฌ๋ชฝ์ ์ ํฌ์๊ฒ ์คํ ๊ฐ๋ฅํ ๋ชฉํ์ฒ๋ผ ๋ณด์์ต๋๋ค.
ํฌ๋ชฝ์ ์ปดํฌ๋ํธ๊ฐ ๋ง์ ๊ด๊ณ๋ก โํฌ๋ชฝ ์ํฐํ๋ผ์ด์ฆ(kmong.com/enterprise)โ์ ํด๋ก ํ์ต๋๋ค. ๊ตฌํ๋ ๊ธฐ๋ฅ์ ๊ฐ๋จํ ๊ฐ์๋ ์๋์ ๊ฐ์ต๋๋ค.
- โํ๋ก์ ํธ ์๋ขฐํ๊ธฐโ ๋ฒํผ ํด๋ฆญ ์, ์์ ์ ํ ํ ๋ฑ๋ก.
- ์๋ขฐ ๋ ํ๋ก์ ํธ๋ค์ ๋ฆฌ์คํธ ๋ฐํ.
- ํ์๊ฐ์ ์งํ๊ณผ ๋ก๊ทธ์ธ.
๋ณด๋ค ์์ธํ ์ฌํญ์ ํ๊ธฐ โ**3. Wireframe - ํฌ๋ชฝโ**์์ ์ฐธ๊ณ ํ์ค ์ ์์ต๋๋ค.
- 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 ํจํด ์ค๊ณ
- Java 8
- SpringBoot
- Spring Security
- Gradle
- JPA
- MySQL 8.0
- AWS S3
- JWT
- OAuth2
- React
- react-router-dom
- Axios
- Redux
- Styeld Component (for es6 and css)
- Fortawesome
- redux-toolkit
- AWS
- FileZilla
https://docs.google.com/spreadsheets/d/1xkkSbZWIB8ChC1NRSAErkekziXt4bmB7EdIGHJpftzs/edit#gid=1824601528
https://www.youtube.com/watch?v=aTwMly1ICzE
-
ํ์๊ฐ์ , ๋ก๊ทธ์ธ & ๋ก๊ทธ์์
- jwt์ ์ฌ์ฉํ ์ ํจ์ฑ ๊ฒ์ฌ
- ์ด๋ฉ์ผ ์์ ์ ๊ท์์ผ๋ก ์ ํจ์ฑ ๊ฒ์ฌ
-
ํ๋ก์ ํธ CRUD
- ๊ฐ ํ๋ก์ ํธ๋ง๋ค ์์ธ ํ์ด์ง ๊ตฌ์ฑ ํ ์ธ๋ถ ์ฌํญ ํ์ธ
- ์ ๋ ฌ ๊ธฐ๋ฅ์ ์ถ๊ฐํ์ฌ ํค์๋ ๋ณ๋ก ํ์ธ ๊ฐ๋ฅ
-
ํํ์ด์ง์ ํ๋ก์ ํธ ๋ฆฌ์คํธ ์กฐํ
- ๊ฐ ์ํฉ์ ๋ง๋ ์ ๋ณด ์ ๋ฌ
-
ํ์ผ ์ ๋ก๋
- ์ฌ๋ฌ ๊ฐ์ ํ์ผ๋ค์ ๋ฆฌ์คํธ ํํ๋ก ์ ๋ฌ ๋ฐ์ s3์ ์ ์ฅ
- ์ดํ ์๋ฒ์์ s3 URL์ ๋์ํ๋ ํ์ผ ์ด๋ฆ๊ณผ ํจ๊ป ์ ์ฅ
- ์์ธ ํ์ด์ง์์ ํ์ผ ํ์ธ ๊ฐ๋ฅ
-
ํ์ผ ์ ๋ก๋(2)
- ํ๋ก ํธ์์ ํ์ผ์ ์ ๋ก๋ํ๊ธฐ ์ํด JSON.stringfy์ Blob์ผ๋ก 2์งํ ๋ฐ์ดํฐ ์์ฑ
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ํ์ฌ ํด๊ฒฐ
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 ๊ฐ์ฒดโ ์ฐธ๊ณ .
-
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];
-
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> ); })}
-
๊ฐ์ฒด๋ฅผ stringfy์ Blob์ผ๋ก ์ฒ๋ฆฌ
const formData = new FormData(); formData.append( "projectDto", new Blob( [JSON.stringify(projectDto, { contentType: "application/json" })], { type: "application/json", } ) ); formData.append("files", file);
-
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); } }
-
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
์ด ๋ฐ์ํ๋ ๊ฒ์ด๋ค.
๊ธฐ๋ฅ | 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:โํ์ผ ์ญ์ ์คํจโ } |
๋๊ตฐ๊ฐ์ ๋ท๋ชจ์ต์ด ๋ณด์ด๊ธฐ ์์ํ๋ ๊ฒ์ ์ฌ๋ ๋๋ฌธ๋ง์ด ์๋๋ผ๋ ๊ฒ์ด ์๋๋ผ๋ ๊ฒ์ ๋ฐฐ์ด ํ ์ฃผ์์ต๋๋ค. ์ง๋ ์ผ ์ฃผ ๋์ ์คํ๋ง์ ๊ณต๋ถํ๋ฉด์ โ์ ๋ด๊ฐ ์ ์ด๋ ๊ฒ ์ด๋ ค์ด ์คํ๋ง์ ์ ํํด์ ์ด๋ ๊ฒ ๊ณ ์ํ๋ ๊ฒ์ผ๊น?โ๋ ์๊ฐ์ด ๋ค์์ต๋๋ค. ์๋ฐ์ ์คํ๋ง์ ๊ฐ์์ ์ด์ง ์์ ๋ฟ๋๋ฌ MVC๊ณผ ์ญํ ๊ณผ ์ฑ ์์ ๋ถํ ํ๋ ์ ๋ง์ ํด๋์ค๋ฅผ ๋ง๋ค์ด์ผํ๊ณ , ๋ณด์์ ์ฃ๋ถ๋ฆฌ ์์ ๋๊ธฐ ์ด๋ ค์ด ์์ค์ด์์ผ๋๊น์.
ํ์ง๋ง ๋ฆฌ์กํธ์ ๊ฐ์ด ํ์ ์ ํ๊ณ ๋๋ ์์์ต๋๋ค. ๋ฆฌ์กํธ๊ฐ ์คํ๋ง๋ณด๋ค ํจ์ฌ ๋ ๋ง์ ์๊ฐ๊ณผ ๋ ธ๋ ฅ์ ์์์ผ ํ๋ก์ ํธ๊ฐ ๋๋ ์ ์๋ค๋ ๊ฒ์ ๋ง์ด์์. ์คํ๋ง์ด ๊ตฌ์กฐ ์ค๊ณํ๊ณ ์ฌ์ ๊ฐ ์์ ๋๋ ๋ฆฌ์กํธ์์๋ ๋จธ๋ฆฌ๋ฅผ ์ฅ์ด์ง๊ณ ์ฝ๋๋ฅผ ์ง๊ณ ์๋๊ฑธ ๋ณด๋ ๊ทธ๋ค์ ๋ฑ์ด ๋ณด์ด๊ธฐ ์์ํ์ต๋๋ค. ๋ฐฑ์๋์ ๊ตฌ์กฐ ์ค๊ณ๋ ๋์ด ์์ง๋ง ์ฌ๋ฏธ์ ์์๊ฐ ๊ฐ๋ฏธ๋ ํ๋ก ํธ์์๋ ๊ทธ ๋์ ์ ํด์ผ ํ๋๊น์.
ํ ์ฃผ ๋์ ์ ์ ์๊ปด๊ฐ๋ฉฐ ํ๋ก์ ํธ๋ฅผ ๋ง๋ฌด๋ฆฌ ํด์ฃผ์ จ๋ ๋ชจ๋ ๋ถ๋ค์๊ฒ ๊ฐ์ฌ ์ธ์ฌ๋ฅผ ๋๋ฆฝ๋๋ค. ํญํด99์ ๋จ์ ๊ธฐ๊ฐ ๋์ ์คํธ๋ ์ค ์์ด ์ํ์๋ ๊ฒฐ๊ณผ ์ป์ผ์๊ธธ ๊ธฐ๋ํ๊ฒ ์ต๋๋ค. - ์ด๋์ฌ
ํด๋ก ์ฝ๋ฉ 1์ฃผ์ผ ๋์ ๋ง์ ๊ฒ๋ค์ ๋ฐฐ์ ๋ ๊ฒ ๊ฐ์ต๋๋ค. ์ ๋ฒ ์ฃผ์ฐจ๊ฐ ํ๋ก ํธ์ ๋ฐฑ ๊ฐ์ ํ์ ์ ๊ฐ๋ ์ ์ค์ฌ์ผ๋ก ์๊ฐํ๋ ๊ด์ ์ ๊ธธ๋ฌ์ฃผ๋ ์ฃผ์ฐจ์๋ค๋ฉด ์ด๋ฒ ์ฃผ์ฐจ๋ ์ค์ ์๋น์ค ์ค์ธ ์ฌ์ดํธ๋ค์ด ์ด๋ค ๋ฐฉํฅ์ผ๋ก ๋น์ฆ๋์ค ๋ก์ง์ด ๊ตฌํ๋์ด ์๋์ง ์ ์ ์๋ ์ฃผ์ฐจ์์ต๋๋ค. ์คํ๋ง์์ ํน์ ๋ก์ง์ ๊ตฌํํ๊ธฐ ์ํด ์ด๋ ํ ํจ์๋ฅผ ์จ์ผ ํ๋๊ฐ, ์น์์ผ, ์คํ๋ง ์ํ๋ฆฌํฐ, S3 ๋ฑ๋ฑ ๋ชฉ์ ์ ๋ง๊ฒ ์ฌ์ฉํ๋ ๊ธฐ๋ฅ ํจ์๋ค์ ์ด๋ ํ ๊ฒ์ธ๊ฐ ์ด๋ฐ ๊ณ ๋ฏผ์ ๋ง์ด ํ๊ฒ ๋ ์ฃผ๊ฐ์ด์์ต๋๋ค. ๋ฌผ๋ก ์ฌ์ ์ ํ๋ก ํธ์ ๋ฐฑ ๊ฐ์ ํ์ ํ์ ๊ตฌํํ์ง ๋ชปํ ๊ธฐ๋ฅ๋ค์ด ๋ง์์ง๋ง ์ถํ ์ค์ ํ๋ก์ ํธ์์๋ ์๊ฐ์ ๊ธธ๊ฒ ์ก๊ณ ๊ตฌํํ์ง ๋ชปํ ๊ธฐ๋ฅ๋ค์ ์ถ๊ฐํด ํด๋ก ์ฝ๋ฉ์์ ์ค์ ์๋น์ค์ ํ์ฌ ๋์ ์ค๋ ฅ ๊ฐ์ ์กด์ฌํ๋ ๊ดด๋ฆฌ๋ฅผ ์ขํ๊ณ ์ถ์ต๋๋ค. ํนํ๋ ๊ณ ์์ด ๋ง์ผ์ จ๋ ํ๋ก ํธ ๋ถ๋ค์๊ฒ ์์์ ๋ฉ์ธ์ง๋ฅผ ๋ณด๋ด๊ณ ์ถ์ต๋๋ค. - ๋ฐ์ธ์ด
์ค์ ํ๋ก์ ํธ ์ ๋ง์ง๋ง ํ ํ๋ก์ ํธ์ด๋ค ๋ณด๋ ์์ฌ์์ด ๊ฝค๋ ๋จ๋๋ค. ์๊ณ ๋ ์์์ง๋ง API ์ค๊ณ์ ์ค์์ฑ์ ๋ค์ ํ๋ฒ ๋๊ผ๋ค. ๊ธํ๊ฒ API ๋ฅผ ์ค๊ณํด์์ธ์ง ์ฝ๋๋ฅผ ์ง๋ ์ค๊ฐ์ค๊ฐ ํ์ด ๋ณด์๊ณ ๊ทธ ํ์ ๊ธํ๊ฒ ๋ฉ๊ฟ๊ฐ๋ฉฐ ์ฝ๋๋ฅผ ์ง๋ค ๋ณด๋ ์ข ๋ ํจ์จ์ ์ด๊ณ ๋ณด๊ธฐ ์ข์ ์ฝ๋์ ๋ํ ๊ณ ๋ฏผ์ด ๋ถ์กฑํ๋ ๊ฒ ๊ฐ๋ค. CRUD ๊ธฐ๋ฅ์ ๋งก๊ฒ ๋๋ฉด์ CRUD ์ ๋ ์ต์ํด์ง๋ ์๊ฐ์ ๊ฐ์ง ์ ์์๊ณ , ํ๋ก ํธ ๋ถ๋ค์ด ์ด๋ค ๋ถ๋ถ์ด ์ ํ์ํ์ง ์์ธํ ์ค๋ช ํด์ฃผ์ ์ ์ด๋ค ์ฌ๋ฃ๋ฅผ ์ฃผ๋ ๊ฒ ๋ ํธํ ์ง์ ๋ํ ๊ฐ์ด ์กํ๋ค. ํ ์ฃผ ๋์ ํจ๊ป ํ๋ด์ฃผ์ จ๋ 8์กฐ ํ์๋ค์๊ฒ ๊ฐ์ฌ๋๋ฆฝ๋๋ค. - ๊น๋ฏผ์ง
์งง์๋ ํด๋ก ์ฝ๋ฉ์ด์ง๋ง, ์ฌ๋ฌ CSS ์์ฑ์ ์ ์ ์์์ต๋๋ค. ์๊ฐ์ด ์งง์ ๋ง์ ๋ถ๋ถ์ ๊ณต๋ถํ์ง ๋ชปํ์ง๋ง ๊ธฐํ๊ฐ ๋๋ค๋ฉด ์ข ๋ ๋ค๋ฌ์ ์ ์๋ ์๊ฐ์ ๋ง๋ค ์ ์์ผ๋ฉด ์ข๊ฒ ์ต๋๋ค. ์ด๋ฒ์ ๊ธฐ๋ฅ ๊ตฌํ์ด ๋ง์ด ๋ฆ์ด์ ธ์ ๋ง์ ๊ธฐ๋ฅ์ ๋ด์ ์ ์์๋ ๋ถ๋ถ์ ๋ํด ๋ฐฑ์๋ ๋ถ๋ค๊ป ์ฌ๊ณผ์ ๋ง์ ๋๋ฆฌ๊ณ ์ถ์ต๋๋ค. - ํ์ง์ฉ
ํด๋ก ํ๋ก์ ํธ๋ฅผ ํ๋ฉด์ ์ค์ ์ฌ์ดํธ๊ฐ ์ผ๋ง๋ ๊ผผ๊ผผํ๊ณ ์ธ๋ฐํ๊ฒ ์ค๊ณ๊ฐ ๋์๋์ง ์ ์ ์์์ต๋๋ค. ํ์์๋ ๊ทธ์ ์ด์ฉํ๊ธฐ๋ง ํ๋ ์ฌ์ดํธ๋ฅผ ํด๋ก ํ๊ธฐ ์ํด ๊ตฌ์กฐ๋ฅผ ์ดํผ๊ณ ๋ง๋๋ ๊ณผ์ ์์ ์ฌ์ฉ์๊ฐ ํธ๋ฆฌํ๊ฒ ์ฌ์ฉํ ์๋ก ๋ค์์ ๊ฐ๋ฐ์๋ค์ ๋ ์ผ์ ๋ง์ด ํด์ผํ๋ ๊ฒ์ด๊ตฌ๋! ๋ฅผ ๊นจ๋ฌ์์ต๋๋ค. ์ ํฌ๊ฐ ์์์ ์์ ํ ๋ ํธํ๊ฒ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ ์ธ ์ ์๋๋ก ๊ฐ์ด ์ด์ฌํ ํ์ ๋ฐฑ์๋ ๋ถ๋ค๋ ๋ชจ๋ ์๊ณ ํ์ จ์ต๋๋ค! -์ด๊ฐ์ฐ
์ค์ ํ๋ก์ ํธ ์ ๊ธฐ๋ณธ CRUD ๊ธฐ๋ฅ์ ๋ํด ์๋ฒฝํ๊ฒ ์ ๋ฆฌํ ์ ์์๋ ์๊ฐ์ด์์ต๋๋ค. ์ค์ ์๋น์ค๊ฐ ํ์ด์ง์ ๊ทธ๋ ค์ง๋ ๊ฒ์ ์ ์๋๋ง ๊ฒฝํํด๋ณด๋ ํ๋ก ํธ ๊ฐ๋ฐ์๋ก์ UX ์ ์ผ๋ก ์ผ๋ง๋ ๋ํ ์ผํ๊ณ ์น๋ฐํ๊ฒ ์ ๊ทผํด์ผํ๋ ์ง๋ฅผ ์๊ฒ ๋์์ต๋๋ค. ๋ค์์ ๋ ๋ ํ๊ฒ ๋ฐ์ณ์ฃผ์๋ ๋ฐฑ ๋ถ๋ค ๋๋ถ์ ๋ง์ ํธํ ํ๋ก ํธ๋ค์ ์๋ง ๋ณด๊ณ ๋ฌ๋ฆด ์ ์์์ต๋๋ค. ํ ์ฃผ๋์ ๋๋ฌด ๊ณ ์๋ง์ผ์ จ๊ณ ๊ฐ์ฌํ์ต๋๋ค! ๋จ์ ํญํด ์๊ฐ๋ ํฌ๋ชฝ์ด๋ค ์ํญํ์ธ์๐ณ - ์กฐํด์