선택 13, SpringBoot로 인별그램 만들기

LeeKyoungChang·2022년 12월 30일
0

📚 1. 문제 소개

📖 A. 제공된 자료

✔️ 학습 목표

  • SpringBoot로 RESTful API 서버를 구축하고 CRUD를 완성한다.
  • 파일 업로드/다운로드 기능을 완성한다.

 

✔️ 스프링 부트, JPA 전체적인 동작 과정

잙려진 사진

사진 참고자료

  • 사용자들이 디바이스에 있는 웹브라우저(크롬)등을 통해서 요청할 시,
  • SpringBoot 서버로 통신하게 되고, 서버에서는 DB와의 CRUD 결과를 클라이언트에게 전달한다.
  • 전달될 시, 결과가 사용자 화면에 띄워진다.

 

🛠️ RestController vs Controller

  • controller는 주로 view (화면) return이 주 목적
  • RestController는 view가 필요없고 API만 지원하는 서비스에서 주로 사용

 

 

📖 B. API 설계

기능METHOD리소스(앤드포인트)PATH인풋데이터인풋 데이터 타입아웃풋 데이터아웃풋 데이터 타입
사진 목록 조회GET/content[{uid:(String), path:(String), contents:(String)}]json list
사진 목록 작성POST/contentpicture:(file), title:(String), password:(String)multipart/form{path:(String)}json
사진 목록 수정PUT/content/{uid}picture:(file), title:(String), password:(String)multipart/form{path:(String)}json
사진 목록 삭제DELETE/content/{uid}password:(String)jsonvoid
  • 총 4개의 기능으로 조회, 등록, 수정, 삭제로 설계
  • 테스트 및 기능 테스트는 Postman 이라는 툴로 진행

 

 

📚 2. 환경 세팅

✔️ 준비할 것

  • JDK 1.8이상의 버전, JAVA 프로젝트 빌드 툴, Postman 툴

사전 학습에 적혀있는 방법으로도 환경 세팅을 할 수 있지만, spring initializer를 사용했다.

스크린샷 2022-12-30 오전 9 41 25
  • Gradle, SpringBoot 2.7.7(3.0.1 할 경우 Java 11버전에서 오류 발생), Jar, 11
  • Sprnig Web, Lombok, Spring Data JPA, MySQL Driver, Spring Boot DevTools

 

✔️ 현재 디렉터리 구조

스크린샷 2022-12-30 오전 10 35 28
  • Intellij IDEA

 

✔️ 문제에서 제공하는 프론트(화면) index.html

<!DOCTYPE html>  
<html>  
<head>  
    <meta charset="UTF-8">  
    <title>crud test</title>  
  
</head>  
<body>  
<div id="app">  
    <input type="text" id="title" ref="title" /><br>  
    <input type="file" id="picture"  ref="picture" accept="image/png, image/jpeg"  /><br>  
    <input type="password" id="password" ref="password" />  <br>  
    <button v-on:click="post" >Posting!</button>  
    <hr>  
    <div  
            v-for="list in lists"  
            v-bind:id="list.uid"  
    >  
        {{list.uid}}  
        <img v-bind:src=list.path width="100%"/>  
        <input type="text" v-model=list.title ref="tt" /><br>  
        <input type="file" v-bind:ref=list.uid accept="image/png, image/jpeg"  /><br>  
        <button v-on:click="update(list)" >update</button>  
        <button v-on:click="del(list)" >delete</button>  
        <hr>  
    </div>  
</div>  
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>  
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/4.0.1/mustache.min.js"></script>  
<script type="text/javascript" >  
    new Vue({  
        el: '#app',  
        data: {  
            file: null  
            , lists: []  
            , path: ''  
            , status: ''  
        },  
        methods: {  
            post: function () {  
                var formData = new FormData();  
                formData.append("picture", this.$refs.picture.files[0] );  
                console.log("files : " + this.$refs.picture.files[0])  
                formData.append("title", this.$refs.title.value );  
                formData.append("password", this.$refs.password.value );  
                axios.post(  
                    '/content'  
                    , formData  
                    , { headers: {  'Content-Type': 'multipart/form-data'   }   }  
                ).then(response => {  
                    var result = response.data;  
  
                    // 결과 출력  
                    for(prop in result)  {  
                        if(prop === "Error") alert(result[prop]);  
                    }                    this.getList();  
                }).catch(error => {  
                    console.log(error);  
                });            }            ,update: function (list) {  
                var upPasswd = prompt("Enter the password for update.");  
                if (!upPasswd){  
                    alert("password를 입력해주세요.");  
                    return;  
                }                var formData = new FormData();  
                formData.append("picture", this.$refs[list.uid][0].files[0]);  
                formData.append("title", list.title );  
                formData.append("password", upPasswd );  
                axios.put(  
                    '/content/'+list.uid  
                    , formData  
                    , { headers: {  'Content-Type': 'multipart/form-data'   }   }  
                ).then(response => {  
                    var result = response.data;  
  
                    // 결과 출력  
                    for(prop in result)  {  
                        if(prop === "Error") alert(result[prop]);  
                    }                    // console.log(response.data);  
                    this.getList();  
                }).catch(error => {  
                    console.log(error);  
                });            }            ,            del: function (list) {  
                var delPasswd = prompt("Enter the password for delete.");  
                if (!delPasswd){  
                    alert("password를 입력해주세요.");  
                    return;  
                }                axios.delete('/content/'+list.uid  
                    ,{ data: { password: delPasswd  }}  
                ).then(response =>{  
                    // var result = response.data;  
                    //                    // // 결과 출력  
                    this.getList()  
                }                ).catch(error =>  
                    console.log(error)  
                );            }            ,            getList: function () {  
                axios.get('/content').then(response => {  
                    this.lists = response.data  
                    response.data.forEach(element => {  
                        // console.log(element.path);  
                    });  
                }).catch(error =>  
                    console.log(error)  
                );            }        }        ,        mounted: function () {  
            this.$nextTick(function () {  
                this.getList();  
            })  
        }  
    })</script>  
</script>  
</body>  
</html>

 

✔️ resources/application.yml

spring:  
  datasource:    url: jdbc:mysql://localhost:3306/studytest?serverTimezone=UTC&useUniCode=yes&characterEncoding=UTF-8  
    username: ssafy  
    password: ssafy  
  
  jpa:  
    hibernate:      ddl-auto: update  
    properties:  
      hibernate:        show_sql: true  
        format_sql: true  
    database-platform: org.hibernate.dialect.MySQL8Dialect
  • 나는 h2보다 mysql에 적용해보고 싶어 mysql을 사용했다.

 

 

📚 3. 과제

📖 A. RESTful API 서버 구축

스크린샷 2022-12-30 오전 10 44 57

 

📖 B. CRUD 메소드 구현 (RestController.java)

✔️ Req. 2-1 사진 목록 조회
로직 : 정의되어진 Repository를 사용하여 DB를 조회를 해온 뒤에 프론트단에 필요한 정보들을 값을 셋팅해서 돌려준다.

스크린샷 2022-12-30 오전 10 56 08 스크린샷 2022-12-30 오전 10 56 18 스크린샷 2022-12-30 오전 10 58 36
@Slf4j  
@RestController  
@AllArgsConstructor  
@RequestMapping("/content")  
public class InstagramRestController {  
  
    @Autowired  
    ContentRepository contentRepository;  
  
    // Req. 2-1 사진 목록 조회 기능  
    @GetMapping  
    public List<Map<String, Object>> list(){  
        List<Map<String, Object>> result = new ArrayList<>();  
        List<InstagramContent> list = contentRepository.findTop1000ByOrderByUidDesc();  
  
        for (InstagramContent instagramContent : list) {  
            Map<String, Object> map = new HashMap<>();  
            log.info("uid : " + instagramContent.getUid() + " path : " + instagramContent.getPath()  
                    + " title : " + instagramContent.getTitle());  
            map.put("uid",instagramContent.getUid());  
            map.put("path",instagramContent.getPath());  
            map.put("title",instagramContent.getTitle());  
            result.add(map);  
        }  
        return result;  
    }

InstagramContent

  
@Entity  
@NoArgsConstructor  
@Getter  
@Setter  
@ToString  
public class InstagramContent {  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    private int uid;  
    private String path;  
    private String title;  
    private String password;  
  
    @Builder  
    public InstagramContent(String path, String title, String password) {  
        this.path = path;  
        this.title = title;  
        this.password = password;  
    }}

ContentRepository

public interface ContentRepository extends JpaRepository<InstagramContent, Integer> {  
  
    public List<InstagramContent> findTop1000ByOrderByUidDesc();  
}

 

✔️ Req. 2-2 사진 목록 작성

로직

  • 파일을 받아서 지정된 폴더에 저장을 한다. 하려고 할 때에 폴더가 없으면 새로 생성한다.
  • 데이터들을 저장한다. Password는 패스워드, path는 파일이름, title은 제목이다.
  • 리턴 값으로 path 값에 파일이름을 넘겨준다.
스크린샷 2022-12-30 오전 10 56 08
  
    // 2-2 사진 목록 작성 기능  
    @PostMapping  
    public Map<String, String> post(  
            @RequestPart("picture") MultipartFile pic,  
            @RequestParam("title") String title,  
            @RequestParam("password") String password) throws IOException {  
        String path = System.getProperty("user.dir");  
        File file = new File(path + "/src/main/resources/static/" + pic.getOriginalFilename());  
  
        if(pic.isEmpty()){  
            log.info("파일 저장");  
            // 파일을 저장한다.  
           return Map.of("Error", "사진을 입력해주세요.");  
        }//        log.info("password : " + password);  
  
        if(password.isEmpty()){  
            return Map.of("Error", "password를 입력해주세요.");  
        }  
        log.info("pic : " + pic.getOriginalFilename());  
        log.info("path와 title, password 입니다 : " + path + " " + title + " " + password);  
  
        // transferTo 메소드를 이용하여 multipartFile에 담겨있는 파일을 해당 경로에 담는다.  
        pic.transferTo(file);  
        // builder를 사용하지 않은 소스 vs builder를 사용한 소스  
//        contentRepository.save(new InstagramContent(pic.getOriginalFilename(), title, password)); // 데이터 저장  
        contentRepository.save(InstagramContent.builder()  
                .password(password)  
                .path(file.getName())  
                .title(title)  
                .build());  
  
        return Map.of("path", file.getName());  
    }

✏️ builder

  • 객체를 생성할 수 있는 빌더를 builder() 함수를 통해 얻는다.
  • 얻고자 하는 값들을 사용하고 마지막에 build()를 통해 빌더를 작동 시켜 객체를 생성한다.

 

✔️ Req. 2-3 사진 목록 수정
로직

  • 프론트단에서 호출된 uid 값으로 Repository를 조회한다.
  • 조회된 객체의 비밀번호와 프론트 단에서 넘어온 비밀번호와 비교를 해서 같으면 계속 진행한다.
  • 프론트단에서 넘어온 것에 파일(사진)이 있을 시에 사진을 미리 지정된 폴더에 업로드하고, 파일 이름을 업데이트 해준다.
  • Title 값을 업데이트 해준다.
  • 저장하고 Path값에 파일이름을 셋해주고 리턴해준다.
스크린샷 2022-12-30 오전 11 15 36 스크린샷 2022-12-30 오전 11 17 29
    // 2-3 사진 목록 수정  
    @PutMapping("/{uid}")  
    public Map<String, String> update(  
            @PathVariable int uid, @RequestPart("picture") MultipartFile pic,  
            @RequestParam("title")String title, @RequestParam("password") String password  
    )throws IOException{  
        InstagramContent content = contentRepository.findById(uid).get();  
  
        // 비밀번호가 다르다면, 에러로 처리  
        if(!password.equals(content.getPassword())){  
            return Map.of("Error","비밀번호 잘못 입력하셨습니다.");  
        }  
  
        // 만약에 조회한 파일이 존재한다면  
        if(!pic.isEmpty()){  
            // 현재 경로  
            String path = System.getProperty("user.dir");  
            File file = new File(path + "/src/main/resources/static/" + pic.getOriginalFilename());  
  
            // static에 이미지 파일 등록  
            pic.transferTo(file);  
//            log.info("name : " + file.getName());  
  
            // 이미지 파일이름 수정  
            content.setPath(file.getName());  
            // title 수정  
            content.setTitle(title);  
        }  
        // 현재 결과 저장  
        contentRepository.save(content);  
  
        return Map.of("path", content.getPath());  
    }

 

✔️ Req. 2-4 글 삭제 기능

로직

  • 프론트단에서 호출된 uid 값으로 Repository를 조회한다.
  • 조회된 객체의 비밀번호와 프론트 단에서 넘어온 비밀번호와 비교를 해서 같으면 삭제를 수행한다.
스크린샷 2022-12-30 오전 11 22 06 스크린샷 2022-12-30 오전 11 22 11(2) 스크린샷 2022-12-30 오전 11 23 03
// 2-4 글 삭제  
@DeleteMapping("/{uid}")  
public void delete(@PathVariable int uid, @RequestBody Map<String, Object> body){  
    // 현재 입력된 비밀번호가 db에 저장된 id에 해당된 비밀번호와 같다면  
    if(body.get("password").toString().equals(contentRepository.findById(uid).get().getPassword())){  
        contentRepository.deleteById(uid);  
    }}

 

 

📚 4. 문제점

📖 A. Postman에서 post 형식으로 데이터 전달할 때 (현재 결과를 보지 못하고 있다.)

@RequestPart에 어떠한 형식으로 데이터를 넣어야 할지 모르겠다.

스크린샷 2022-12-30 오전 11 27 54 스크린샷 2022-12-30 오전 11 28 02 스크린샷 2022-12-30 오전 11 30 08 스크린샷 2022-12-30 오전 11 30 13
  • Headers, Body - raw까지 구글링 해보며 여러가지 값을 넣어보았지만, 해결하지 못했다.

 

 

📖 B. 새로운 이미지를 추가할 때마다 이전 실행 상태에서 이미지가 추가되기 때문에 화면에 뜨지 않는 오류가 발생합니다.

이전에 추가했던 사진들은 화면에 잘 나타나지만, 새롭게 추가했던 사진은 화면에 잘 나타지 않습니다.

이유로는

스크린샷 2022-12-30 오전 11 33 58
  • Intellij가 실행될 때 resources/static에 있는 png 파일을 선택해서 화면에 출력할 때는 올바른 결과가 나오지만
  • 아직 resources/static에 없는 png를 새로 추가할 경우, 화면에 해당 사진이 출력되지 않습니다.
스크린샷 2022-12-30 오후 1 31 58

이에 대한 오류를 찾고 있습니다.

 

 

📚 5. 느낀점

  • 1학기 동안 배웠던 spring boot에서는 데이터를 db에 저장하기 위해서는 직접 sql문을 작성해야 했습니다.
    • (dto 데이터 받고) controller -> service -> dao -> mybatis(직접 sql 작성)
  • 이제는 jpa가 제공하는 API를 사용하면 객체를 DB에 저장하고 관리할 때, 개발자가 직접 작성하지 않아도 된다는 것을 알게 되었습니다.
    • (dto 데이터 받고) controller -> service -> repository, entity -> db

 

2학기 프로젝트에 이를 적용해볼 예정이며, 이에 더 나아가 Filter, Interceptor, AOP 동작과정을 적용해보며 만들어볼 것입니다.

 

 

profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글