diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/mutsa_sns_review/.gitignore b/mutsa_sns_review/.gitignore new file mode 100644 index 0000000..fef64e7 --- /dev/null +++ b/mutsa_sns_review/.gitignore @@ -0,0 +1,154 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ +# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,java +# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,java + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +# End of https://www.toptal.com/developers/gitignore/api/intellij+all,java + +*.sql +*.sqlite +/media \ No newline at end of file diff --git a/mutsa_sns_review/Project-API-part3.postman_collection.json b/mutsa_sns_review/Project-API-part3.postman_collection.json new file mode 100644 index 0000000..566602b --- /dev/null +++ b/mutsa_sns_review/Project-API-part3.postman_collection.json @@ -0,0 +1,542 @@ +{ + "info": { + "_postman_id": "e104a08b-6918-4d55-8bd1-04c82b4e0ed8", + "name": "Project-API-part3", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "28054685" + }, + "item": [ + { + "name": "회원가입 & JWT 발급 & 프로필사진 업로드 API : Users", + "item": [ + { + "name": "[로그인(토큰발급)] : POST /users/login", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\" : \"dohun\",\n \"password\" : \"1234\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/users/login", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "login" + ] + } + }, + "response": [] + }, + { + "name": "[로그인 2번 유저 테스트용] : POST /users/login", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\" : \"minsu\",\n \"password\" : \"1212\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/users/login", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "login" + ] + } + }, + "response": [] + }, + { + "name": "[회원 가입] : POST /users/register", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\" : \"dohun\",\n \"password\" : \"1234\",\n \"passwordCheck\" : \"1234\",\n \"email\" : \"dohun@gmail.com\",\n \"phone\" : \"010-1234-1234\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/users/register", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "register" + ] + } + }, + "response": [] + }, + { + "name": "[회원 가입 2번 유저 테스트용] : POST /users/register", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\" : \"minsu\",\n \"password\" : \"1212\",\n \"passwordCheck\" : \"1212\",\n \"email\" : \"minsu@gmail.com\",\n \"phone\" : \"010-1212-1212\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/users/register", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "register" + ] + } + }, + "response": [] + }, + { + "name": "[JWT 발급] : POST /token/issue Copy", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJkb2h1biIsImlhdCI6MTY5MDkwNzE2MiwiZXhwIjoxNjkwOTEwNzYyfQ.txfIkZGmzHk1fCEhGD03qepz1HJE4ccg35qpW-jvUGnko8NKTekoEF4rRN8wzYIbuTQh3TpCdMt5dJqFGEA-_Q", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"username\" : \"dohun\",\n \"password\" : \"1234\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/token/issue", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "token", + "issue" + ] + } + }, + "response": [] + }, + { + "name": "[프로필 사진 업로드] : POST /users/update-image", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJkb2h1biIsImlhdCI6MTY5MTQ5NjM3NiwiZXhwIjoxNjkxNDk5OTc2fQ.Y3rqHYwXnPG7H3P79-xTnMC8onoayEMIK0H6Dw0QqGwQXai_Gv_jDEUg3xfpud2B", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "image", + "type": "file", + "src": "/Users/dohun/Pictures/Arthur/excited.jpg" + } + ] + }, + "url": { + "raw": "http://localhost:8080/users/update-image", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "users", + "update-image" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "피드 구현 API : Articles", + "item": [ + { + "name": "[피드 생성] : POST /articles", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJkb2h1biIsImlhdCI6MTY5MTQ5NTk2NywiZXhwIjoxNjkxNDk5NTY3fQ.wkyeFn-86d7ZlZza2rD2_CiDXYuazI6rwGkb_H_8HHap0xu_b8Ug92zZ8lXQFbXS", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "title", + "value": "오운완", + "type": "text" + }, + { + "key": "content", + "value": "완료!", + "type": "text" + }, + { + "key": "representativeImage", + "type": "file", + "src": "/Users/dohun/Pictures/Arthur/proof1-white.png" + }, + { + "key": "images", + "type": "file", + "src": "/Users/dohun/Pictures/Arthur/sad1.jpg" + } + ] + }, + "url": { + "raw": "http://localhost:8080/articles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "articles" + ] + } + }, + "response": [] + }, + { + "name": "[피드 목록 조회] : GET /articles", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/articles", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "articles" + ] + } + }, + "response": [] + }, + { + "name": "[피드 단독 조회 & 댓글 조회] : GET /articles/{articleId}", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/articles/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "articles", + "1" + ] + } + }, + "response": [] + }, + { + "name": "[피드 수정] : PUT /articles/{articleId}", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJkb2h1biIsImlhdCI6MTY5MTQ5NTk2NywiZXhwIjoxNjkxNDk5NTY3fQ.wkyeFn-86d7ZlZza2rD2_CiDXYuazI6rwGkb_H_8HHap0xu_b8Ug92zZ8lXQFbXS", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "title", + "value": "즐거운 여행", + "type": "text" + }, + { + "key": "content", + "value": "즐겁다!", + "type": "text" + }, + { + "key": "representativeImage", + "type": "file", + "src": "/Users/dohun/Pictures/Arthur/standing.jpg" + }, + { + "key": "images", + "type": "file", + "src": "/Users/dohun/Pictures/Arthur/ugly.jpg" + } + ] + }, + "url": { + "raw": "http://localhost:8080/articles/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "articles", + "1" + ] + } + }, + "response": [] + }, + { + "name": "[피드 삭제] : DELETE /articles/{articleId}", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJkb2h1biIsImlhdCI6MTY5MTQ5NTk2NywiZXhwIjoxNjkxNDk5NTY3fQ.wkyeFn-86d7ZlZza2rD2_CiDXYuazI6rwGkb_H_8HHap0xu_b8Ug92zZ8lXQFbXS", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:8080/articles/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "articles", + "1" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "피드 댓글구현 API : Comments", + "item": [ + { + "name": "[댓글 등록] : POST /comments/{articleId}", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJtaW5zdSIsImlhdCI6MTY5MTQ5NTk4OCwiZXhwIjoxNjkxNDk5NTg4fQ.AwoeACGmao6zA9eYmTJgnCxyRrMHoChem_PfugTs4pS4nHuzVB2xatWz3S3spLuU", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"content\" : \"좋네요\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/comments/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "comments", + "1" + ] + } + }, + "response": [] + }, + { + "name": "[댓글 수정] : PUT /comments/{commentId}", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJtaW5zdSIsImlhdCI6MTY5MTQ5NTk4OCwiZXhwIjoxNjkxNDk5NTg4fQ.AwoeACGmao6zA9eYmTJgnCxyRrMHoChem_PfugTs4pS4nHuzVB2xatWz3S3spLuU", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"content\" : \"너무 좋네요\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/comments/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "comments", + "1" + ] + } + }, + "response": [] + }, + { + "name": "[댓글 삭제] : DELETE : /comments/{commentId}", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJtaW5zdSIsImlhdCI6MTY5MTQ5NTk4OCwiZXhwIjoxNjkxNDk5NTg4fQ.AwoeACGmao6zA9eYmTJgnCxyRrMHoChem_PfugTs4pS4nHuzVB2xatWz3S3spLuU", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:8080/comments/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "comments", + "1" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "피드 좋아요 기능 API : Comments", + "item": [ + { + "name": "[피드 좋아요] : POST /likes/articles/1", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJtaW5zdSIsImlhdCI6MTY5MTQ5NTk4OCwiZXhwIjoxNjkxNDk5NTg4fQ.AwoeACGmao6zA9eYmTJgnCxyRrMHoChem_PfugTs4pS4nHuzVB2xatWz3S3spLuU", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8080/likes/articles/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "likes", + "articles", + "1" + ] + } + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/mutsa_sns_review/README.md b/mutsa_sns_review/README.md new file mode 100644 index 0000000..be843cd --- /dev/null +++ b/mutsa_sns_review/README.md @@ -0,0 +1,194 @@ +# _📮멋사SNS📮️_ + +## **_💁🏻 프로젝트 소개_** + +> 💡 여러분들이 많이 사용하고 있는 SNS 서비스 등을 착안하여 SNS 플랫폼을 만들어보는 미션형 개인 프로젝트입니다. +> 사용자가 회원가입과 로그인을 하고, 대표사진, 프로필사진을 업로드 할 수 있으며 피드를 작성 할 수 있고 +> 댓글과 좋아요, 팔로우 기능도 사용 할 수 있는 간단한 SNS 플랫폼의 백엔드 서비스입니다. + + + +📚 멋사SNS ERD + + +![erd1](https://github.com/likelion-backend-5th/MiniProject_Basic_KimDohun/assets/80811887/7fa33a33-b315-4061-aa4a-0e7a9d656715) + + + + +📌 요구사항 + + + + + + + + + + + + + + + + +--- + + +## ⏱️ _개발 기간_ + +- `개발 날짜: 2023.08.03 ~ 2023.08.08` + +## 🛠️ _개발 환경_ + +- _IDE: IntelliJ IDEA Ultimate_ +- _Language : Java_ +- _Tech ( dependency )_ + - _Spring Web_ + - _Spring Boot DevTools_ + - _Spring Security_ + - _jjwt_ + - _Spring Data JPA_ + - _Lombok_ + - _SQLite_ + +## 📮 _API Documentations_ + +- [_**View API**_](https://documenter.getpostman.com/view/28054685/2s9Xxzusew) + + + +--- + + +## 🔄 _History_ + +### 📅 _Day 1_ + + +#### 1. _**프로젝트 세팅하기**_ + +- dependency setting +- .yaml setting + +#### 2. _**User 회원가입과 로그인시 Jwt 발급**_ + +- 회원가입이 가능하고 로그인시 jwt 를 발급해서 이 이후의 서비스에 토큰 인증 방식으로 사용함 +- 로그인한 상태에서 프로필 이미지를 등록할 수 있음 + + +### 📅 _Day 2_ + + +#### 1. **_Article entity, repository 추가_** + +#### 2. **_Article 피드 crud 구현_** + +- 피드 생성, 목록조회, 단일조회, 피드 수정, 피드 삭제(표시만) + +#### 3. **_SecurityConfig 수정_** + + +### 📅 _Day 3_ + + +#### 1. **_Comment & Like 기능 추가_** + +- 피드 댓글 등록, 수정, 삭제 +- 댓글 조회는 피드 단독조회시 조회가능 +- 피드 좋아요 기능 : 피드 생성자는 좋아요를 누를 수 없음 + +#### 2. **_deleted_at 수정_** + +#### 3. **_Postman Collection 업로드 & Readme update & 설명 주석 추가_** + +### 📅 _Day 4_ + +- 미구현 + + + + +--- + \ No newline at end of file diff --git a/mutsa_sns_review/build.gradle b/mutsa_sns_review/build.gradle new file mode 100644 index 0000000..a72ea50 --- /dev/null +++ b/mutsa_sns_review/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.1.2' + id 'io.spring.dependency-management' version '1.1.2' +} + +group = 'com.project' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + annotationProcessor 'org.projectlombok:lombok' + + // jjwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // sqlite + runtimeOnly 'org.xerial:sqlite-jdbc:3.41.2.2' + runtimeOnly 'org.hibernate.orm:hibernate-community-dialects:6.2.4.Final' + + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/mutsa_sns_review/gradle/wrapper/gradle-wrapper.properties b/mutsa_sns_review/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9f4197d --- /dev/null +++ b/mutsa_sns_review/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/mutsa_sns_review/gradlew b/mutsa_sns_review/gradlew new file mode 100755 index 0000000..fcb6fca --- /dev/null +++ b/mutsa_sns_review/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/mutsa_sns_review/gradlew.bat b/mutsa_sns_review/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/mutsa_sns_review/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mutsa_sns_review/settings.gradle b/mutsa_sns_review/settings.gradle new file mode 100644 index 0000000..3c5a076 --- /dev/null +++ b/mutsa_sns_review/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'mutsa_sns' diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/MutsaSnsApplication.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/MutsaSnsApplication.java new file mode 100644 index 0000000..c5c6b5f --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/MutsaSnsApplication.java @@ -0,0 +1,13 @@ +package com.project.mutsa_sns; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MutsaSnsApplication { + + public static void main(String[] args) { + SpringApplication.run(MutsaSnsApplication.class, args); + } + +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/config/WebSecurityConfig.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/config/WebSecurityConfig.java new file mode 100644 index 0000000..3e9d29d --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/config/WebSecurityConfig.java @@ -0,0 +1,74 @@ +package com.project.mutsa_sns.config; + +import com.project.mutsa_sns.jwt.JwtTokenFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.intercept.AuthorizationFilter; + +@Configuration +public class WebSecurityConfig { + private final JwtTokenFilter jwtTokenFilter; + + public WebSecurityConfig(JwtTokenFilter jwtTokenFilter) { + this.jwtTokenFilter = jwtTokenFilter; + } + + // 애플리케이션의 보안 필터와 인증 설정을 구성 + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + // CSRF (Cross-Site Request Forgery) 보호를 비활성화하여 stateless 인증에 맞게 설정 + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests( + authHttp -> authHttp + // 인증을 필요로 하지 않는 공개 엔드포인트를 정의 + .requestMatchers( + "/users/login", + "/users/register", + "/token/issue" + ) + .permitAll() + .requestMatchers( + HttpMethod.GET, "/articles" + ).permitAll() + .requestMatchers( + HttpMethod.GET, "/articles/**" + ).permitAll() + .requestMatchers( + "/users/update-image", + "/articles", + "/articles/**", + "/comments", + "/comments/**", + "/likes/**" + ) + .authenticated() + .anyRequest() + .authenticated() + ) + // 세션 관리를 stateless (JWT 인증)으로 설정 + .sessionManagement( + sessionManagement -> sessionManagement + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + // 커스텀한 JWT 토큰 필터를 AuthorizationFilter 클래스 앞에 추가 + .addFilterBefore( + jwtTokenFilter, + AuthorizationFilter.class + ); + return http.build(); + } + + // 비밀번호 암호화.. 해싱 및 검증에 사용 + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/ArticleController.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/ArticleController.java new file mode 100644 index 0000000..cb08d16 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/ArticleController.java @@ -0,0 +1,71 @@ +package com.project.mutsa_sns.controller; + +import com.project.mutsa_sns.dto.ArticleDetailResponseDto; +import com.project.mutsa_sns.dto.ArticleListResponseDto; +import com.project.mutsa_sns.dto.ResponseDto; +import com.project.mutsa_sns.service.ArticleService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/articles") +public class ArticleController { + + private final ArticleService articleService; + + public ArticleController(ArticleService articleService) { + this.articleService = articleService; + } + + // 게시물 생성 엔드포인트 + @PostMapping + public ResponseEntity createArticle( + @RequestParam String title, + @RequestParam String content, + @RequestParam MultipartFile representativeImage, + @RequestParam List images + ) { + ResponseDto response = articleService.createArticle(title, content, representativeImage, images); + return ResponseEntity.ok(response); + } + + // 게시물 목록 조회 엔드포인트 + @GetMapping + public ResponseEntity> getArticleList() { + List responseDtoList = articleService.getArticleList(); + return ResponseEntity.ok(responseDtoList); + } + + // 게시물 상세 조회 엔드포인트 + @GetMapping("/{articleId}") + public ResponseEntity getArticleDetail(@PathVariable Long articleId) { + ArticleDetailResponseDto responseDto = articleService.getArticleDetail(articleId); + return ResponseEntity.ok(responseDto); + } + + // 게시물 수정 엔드포인트 + @PutMapping("/{articleId}") + public ResponseEntity updateArticle( + @PathVariable Long articleId, + @RequestParam String title, + @RequestParam String content, + @RequestParam MultipartFile representativeImage, + @RequestParam List images + ) { + ResponseDto response = articleService.updateArticle(articleId, title, content, representativeImage, images); + return ResponseEntity.ok(response); + } + + // 게시물 삭제 엔드포인트 + @DeleteMapping("/{articleId}") + public ResponseEntity deleteArticle(@PathVariable Long articleId) { + ResponseDto response = articleService.deleteArticle(articleId); + return ResponseEntity.ok(response); + } +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/CommentController.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/CommentController.java new file mode 100644 index 0000000..da6db62 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/CommentController.java @@ -0,0 +1,45 @@ +package com.project.mutsa_sns.controller; + +import com.project.mutsa_sns.dto.CommentRequestDto; +import com.project.mutsa_sns.dto.ResponseDto; +import com.project.mutsa_sns.service.CommentService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/comments") +public class CommentController { + + private final CommentService commentService; + + public CommentController(CommentService commentService) { + this.commentService = commentService; + } + + // 댓글 생성 엔드포인트 + @PostMapping("/{articleId}") + public ResponseEntity createComment( + @PathVariable Long articleId, + @RequestBody CommentRequestDto requestDto + ) { + ResponseDto response = commentService.createComment(articleId, requestDto); + return ResponseEntity.ok(response); + } + + // 댓글 수정 엔드포인트 + @PutMapping("/{commentId}") + public ResponseEntity updateComment( + @PathVariable Long commentId, + @RequestBody CommentRequestDto requestDto + ) { + ResponseDto response = commentService.updateComment(commentId, requestDto); + return ResponseEntity.ok(response); + } + + // 댓글 삭제 엔드포인트 + @DeleteMapping("/{commentId}") + public ResponseEntity deleteComment(@PathVariable Long commentId) { + ResponseDto response = commentService.deleteComment(commentId); + return ResponseEntity.ok(response); + } +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/LikeController.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/LikeController.java new file mode 100644 index 0000000..558e330 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/LikeController.java @@ -0,0 +1,22 @@ +package com.project.mutsa_sns.controller; + +import com.project.mutsa_sns.dto.LikeResponseDto; +import com.project.mutsa_sns.service.LikeService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/likes") +@RequiredArgsConstructor +public class LikeController { + private final LikeService likeService; + + // 특정 게시물에 좋아요 토글 엔드포인트 + @PostMapping("/articles/{articleId}") + public LikeResponseDto likeArticle(@PathVariable Long articleId) { + return likeService.toggleArticleLike(articleId); + } +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/TokenController.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/TokenController.java new file mode 100644 index 0000000..96e0cdd --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/TokenController.java @@ -0,0 +1,46 @@ +package com.project.mutsa_sns.controller; + +import com.project.mutsa_sns.jwt.JwtRequestDto; +import com.project.mutsa_sns.jwt.JwtTokenDto; +import com.project.mutsa_sns.jwt.JwtTokenUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.UserDetailsManager; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +@Slf4j +@RestController +@RequestMapping("token") // http://localhost:8080/token/** 부터 시작하는 요청들 +@RequiredArgsConstructor +public class TokenController { + // UserDetailsManager: 사용자 정보 회수 + // PasswordEncoder: 비밀번호 대조용 인코더 + private final UserDetailsManager manager; + private final PasswordEncoder passwordEncoder; + + private final JwtTokenUtils jwtTokenUtils; + + // /token/issue: JWT 발급용 Endpoint + @PostMapping("/issue") + public JwtTokenDto issueJwt(@RequestBody JwtRequestDto dto) { + // 사용자 정보 회수 + UserDetails userDetails + = manager.loadUserByUsername(dto.getUsername()); + // 기록된 비밀번호와 실제 비밀번호가 다를때 + // passwordEncoder.matches(rawPassword, encodedPassword) + // 평문 비밀번호와 암호화 비밀번호를 비교할 수 있다. + if (!passwordEncoder.matches(dto.getPassword(), userDetails.getPassword())) + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + + JwtTokenDto response = new JwtTokenDto(); + response.setToken(jwtTokenUtils.generateToken(userDetails)); + return response; + } +} \ No newline at end of file diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/UserController.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/UserController.java new file mode 100644 index 0000000..75ccb90 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/controller/UserController.java @@ -0,0 +1,59 @@ +package com.project.mutsa_sns.controller; + +import com.project.mutsa_sns.dto.LoginRequestDto; +import com.project.mutsa_sns.dto.RegisterRequestDto; +import com.project.mutsa_sns.dto.ResponseDto; +import com.project.mutsa_sns.jwt.JwtTokenDto; +import com.project.mutsa_sns.service.UserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +@Slf4j +@RestController +@RequestMapping("/users") +public class UserController { + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + // 로그인 엔드포인트 + @PostMapping("/login") + public ResponseEntity login( + @RequestBody LoginRequestDto loginRequestDto + ) { + JwtTokenDto jwtTokenDto = userService.loginUser(loginRequestDto); + return ResponseEntity.ok(jwtTokenDto); + } + + // 회원가입 엔드포인트 + @PostMapping("/register") + public ResponseEntity register( + @RequestBody RegisterRequestDto registerRequestDto + ) { + // 입력한 패스워드와 패스워드 확인이 일치하지 않으면 예외 발생 + if (!registerRequestDto.getPassword().equals(registerRequestDto.getPasswordCheck())) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "패스워드와 패스워드 확인이 일치하지 않습니다"); + } + ResponseDto responseDto = userService.registerUser(registerRequestDto); + return ResponseEntity.ok(responseDto); + } + + // 프로필 이미지 업데이트 엔드포인트 + @PutMapping(value = "/update-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity userUpdateImage( + @RequestParam(value = "image") MultipartFile multipartFile + ) { + // 이미지 업로드 서비스 호출 + ResponseDto responseDto = userService.uploadProfileImage(multipartFile); + + return ResponseEntity.ok(responseDto); + } +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/detail/CustomUserDetails.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/detail/CustomUserDetails.java new file mode 100644 index 0000000..20e6e49 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/detail/CustomUserDetails.java @@ -0,0 +1,125 @@ +package com.project.mutsa_sns.detail; + +import com.project.mutsa_sns.entity.UserEntity; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +/* + UserDetails 인터페이스를 구현한 사용자 정보 클래스 + Spring Security 에서 사용자의 인증과 권한을 다루기 위해 + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CustomUserDetails implements UserDetails { + private Long id; // 사용자의 ID + private String username; // 사용자의 이름 (아이디) + private String password; // 사용자의 암호화된 비밀번호 + private String profile_img; // 사용자의 프로필 이미지 경로 + private String email; // 사용자의 이메일 + private String phone; // 사용자의 전화번호 + + // 사용자의 권한 정보를 반환하는 메서드 + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return null; + } + + // 사용자의 이름(아이디)을 반환하는 메서드 + @Override + public String getUsername() { + return this.username; + } + + // 사용자의 암호화된 비밀번호를 반환하는 메서드 + @Override + public String getPassword() { + return this.password; + } + + public Long getId() { + return id; + } + + public String getProfile_img() { + return profile_img; + } + + public String getEmail() { + return email; + } + + public String getPhone() { + return phone; + } + + // 사용자의 계정이 만료되지 않았음을 나타내는 메서드 + // true 로 변경하여 체크를 사용하지 않도록 함 + @Override + public boolean isAccountNonExpired() { + return true; + } + + // 사용자의 계정이 잠기지 않았음을 나타내는 메서드 + // true 로 변경하여 체크를 사용하지 않도록 함 + @Override + public boolean isAccountNonLocked() { + return true; + } + + // 사용자의 인증 정보가 만료되지 않았음을 나타내는 메서드 + // true 로 변경하여 체크를 사용하지 않도록 함 + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + // 사용자의 계정이 활성화되었음을 나타내는 메서드 + // true 로 변경하여 체크를 사용하지 않도록 함 + @Override + public boolean isEnabled() { + return true; + } + + // CustomUserDetails 객체를 UserEntity 객체로 변환하는 메서드 + public static CustomUserDetails fromEntity(UserEntity entity) { + CustomUserDetails details = new CustomUserDetails(); + details.id = entity.getId(); + details.username = entity.getUsername(); + details.password = entity.getPassword(); + details.profile_img = entity.getProfile_img(); + details.email = entity.getEmail(); + details.phone = entity.getPhone(); + return details; + } + + // CustomUserDetails 객체를 UserEntity 객체로 변환하는 메서드 + public UserEntity newEntity() { + UserEntity entity = new UserEntity(); + entity.setUsername(username); + entity.setPassword(password); + entity.setProfile_img(profile_img); + entity.setEmail(email); + entity.setPhone(phone); + return entity; + } + + // CustomUserDetails 객체의 문자열 표현을 반환하는 메서드 + // 디버깅 or 로깅 + @Override + public String toString() { + return "CustomUserDetails{" + + "id=" + id + + ", username='" + username + '\'' + + ", password='" + password + '\'' + + ", profile_img='" + profile_img + '\'' + + ", email='" + email + '\'' + + ", phone='" + phone + '\'' + + '}'; + } +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/detail/JpaUserDetailsManager.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/detail/JpaUserDetailsManager.java new file mode 100644 index 0000000..6d6f563 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/detail/JpaUserDetailsManager.java @@ -0,0 +1,77 @@ +package com.project.mutsa_sns.detail; + +import com.project.mutsa_sns.entity.UserEntity; +import com.project.mutsa_sns.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.provisioning.UserDetailsManager; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Optional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class JpaUserDetailsManager implements UserDetailsManager { + + private final UserRepository userRepository; + + // 사용자 이름을 통해 사용자 정보를 불러옴 + // 사용자 이름에 해당하는 사용자를 찾을 수 없는 경우 예외 발생 + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + Optional optionalUser = userRepository.findByUsername(username); + if (optionalUser.isEmpty()) + throw new UsernameNotFoundException(username); + return CustomUserDetails.fromEntity(optionalUser.get()); + } + + // 새로운 사용자를 저장하는 메서드 + // 이미 존재하는 사용자명일 경우 BAD_REQUEST 예외 발생 + // 사용자 정보를 CustomUserDetails 로 캐스팅할 수 없는 경우 + // INTERNAL_SERVER_ERROR 예외 발생 + @Override + public void createUser(UserDetails user) { + log.info("try create user: {}", user.getUsername()); + if (this.userExists(user.getUsername())) { + log.info("Username: {} already exists", user.getUsername()); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Username already exists"); + } + try { + userRepository.save(((CustomUserDetails) user).newEntity()); + } catch (ClassCastException e) { + log.error("failed to cast to {}", CustomUserDetails.class); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to cast user to CustomUserDetails"); + } + } + + // 사용자 이름을 통해 해당 사용자가 존재하는지 확인하는 메서드 + // 사용자가 존재하면 true, 존재하지 않으면 false 반환 + @Override + public boolean userExists(String username) { + log.info("check if user: {} exists", username); + return this.userRepository.existsByUsername(username); + } + + + // TODO -- 미구현 -- + + @Override + public void updateUser(UserDetails user) { + throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED); + } + + @Override + public void deleteUser(String username) { + throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED); + } + + @Override + public void changePassword(String oldPassword, String newPassword) { + throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED); + } +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/ArticleDetailResponseDto.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/ArticleDetailResponseDto.java new file mode 100644 index 0000000..5254e95 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/ArticleDetailResponseDto.java @@ -0,0 +1,18 @@ +package com.project.mutsa_sns.dto; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +public class ArticleDetailResponseDto { + private Long id; // 게시물 ID + private Long userId; // 작성자 사용자 ID + private String username; // 작성자 사용자명 + private String title; // 게시물 제목 + private String content; // 게시물 내용 + private LocalDateTime deletedAt;// 게시물 삭제 일시 + private List imageUrl; // 게시물 이미지 URL 목록 + private List comments; // 댓글 목록 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/ArticleListResponseDto.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/ArticleListResponseDto.java new file mode 100644 index 0000000..cc0cd7f --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/ArticleListResponseDto.java @@ -0,0 +1,12 @@ +package com.project.mutsa_sns.dto; + +import lombok.Data; + +@Data +public class ArticleListResponseDto { + private Long id; // 게시물 ID + private Long userId; // 작성자 사용자 ID + private String username; // 작성자 사용자명 + private String title; // 게시물 제목 + private String imageUrl; // 대표 이미지 URL +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/ArticleRequestDto.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/ArticleRequestDto.java new file mode 100644 index 0000000..2791d90 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/ArticleRequestDto.java @@ -0,0 +1,9 @@ +package com.project.mutsa_sns.dto; + +import lombok.Data; + +@Data +public class ArticleRequestDto { + private String title; // 게시물 제목 + private String content; // 게시물 내용 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/CommentRequestDto.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/CommentRequestDto.java new file mode 100644 index 0000000..247c938 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/CommentRequestDto.java @@ -0,0 +1,8 @@ +package com.project.mutsa_sns.dto; + +import lombok.Data; + +@Data +public class CommentRequestDto { + private String content; // 댓글 내용 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/CommentResponseDto.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/CommentResponseDto.java new file mode 100644 index 0000000..9d6828c --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/CommentResponseDto.java @@ -0,0 +1,13 @@ +package com.project.mutsa_sns.dto; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class CommentResponseDto { + private Long id; // 댓글 ID + private Long userId; // 작성자의 사용자 ID + private String username; // 작성자의 사용자 이름 + private String content; // 댓글 내용 + private LocalDateTime deletedAt; // 삭제된 시간 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/LikeResponseDto.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/LikeResponseDto.java new file mode 100644 index 0000000..b09185a --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/LikeResponseDto.java @@ -0,0 +1,16 @@ +package com.project.mutsa_sns.dto; + +import lombok.Data; + +@Data +public class LikeResponseDto { + + private boolean success; // 성공 여부 + private String message; // 결과 메시지 + + // 생성자 + public LikeResponseDto(boolean success, String message) { + this.success = success; + this.message = message; + } +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/LoginRequestDto.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/LoginRequestDto.java new file mode 100644 index 0000000..a4c50c8 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/LoginRequestDto.java @@ -0,0 +1,9 @@ +package com.project.mutsa_sns.dto; + +import lombok.Data; + +@Data +public class LoginRequestDto { + private String username; // 사용자명 + private String password; // 비밀번호 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/RegisterRequestDto.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/RegisterRequestDto.java new file mode 100644 index 0000000..9e00a03 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/RegisterRequestDto.java @@ -0,0 +1,13 @@ +package com.project.mutsa_sns.dto; + +import lombok.Data; + +@Data +public class RegisterRequestDto { + private Long id; // 아이디 (자동 생성 등의 경우) + private String username; // 사용자명 + private String password; // 비밀번호 + private String passwordCheck; // 비밀번호 확인 + private String email; // 이메일 + private String phone; // 전화번호 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/ResponseDto.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/ResponseDto.java new file mode 100644 index 0000000..1ba1e54 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/dto/ResponseDto.java @@ -0,0 +1,12 @@ +package com.project.mutsa_sns.dto; + +import lombok.Data; + +@Data +public class ResponseDto { + private String message; // API 응답 메시지를 저장하는 클래스 + + public ResponseDto(String message) { + this.message = message; // 응답 메시지를 받아 초기화하는 생성자 + } +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/ArticleEntity.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/ArticleEntity.java new file mode 100644 index 0000000..2d78f57 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/ArticleEntity.java @@ -0,0 +1,48 @@ +package com.project.mutsa_sns.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Data +@Entity +@Table(name = "article") +public class ArticleEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 피드 아이디 + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private UserEntity user; // 작성자 정보 + + @Column(nullable = false) + private String title; // 피드 제목 + + @Column(nullable = false) + private String content; // 피드 내용 + + @Column + private boolean draft; // 초안 여부 + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; // 삭제 시간 + + @OneToMany(mappedBy = "article") + private List articleImages = new ArrayList<>(); // 피드 이미지 리스트 + + @OneToMany(mappedBy = "article") + private List comments = new ArrayList<>(); // 댓글 리스트 + + @ManyToMany + @JoinTable( + name = "article_likes", + joinColumns = @JoinColumn(name = "article_id"), + inverseJoinColumns = @JoinColumn(name = "user_id") + ) + private List likes = new ArrayList<>(); // 좋아요한 사용자 리스트 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/ArticleImageEntity.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/ArticleImageEntity.java new file mode 100644 index 0000000..68e0d67 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/ArticleImageEntity.java @@ -0,0 +1,21 @@ +package com.project.mutsa_sns.entity; + +import jakarta.persistence.*; +import lombok.Data; + +@Entity +@Data +@Table(name = "Article_Images") +public class ArticleImageEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 이미지 아이디 + + @Column(nullable = false) + private String imageUrl; // 이미지 URL + + @ManyToOne + @JoinColumn(name = "article_id", nullable = false) + private ArticleEntity article; // 해당 이미지가 속한 피드 정보 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/CommentEntity.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/CommentEntity.java new file mode 100644 index 0000000..ffc2288 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/CommentEntity.java @@ -0,0 +1,29 @@ +package com.project.mutsa_sns.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.time.LocalDateTime; + +@Entity +@Data +public class CommentEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 댓글 아이디 + + @ManyToOne + @JoinColumn(name = "article_id") + private ArticleEntity article; // 댓글이 속한 피드 정보 + + @ManyToOne + @JoinColumn(name = "user_id") + private UserEntity user; // 댓글 작성자 정보 + + @Column(nullable = false) + private String content; // 댓글 내용 + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; // 댓글 삭제 일시 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/LikeEntity.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/LikeEntity.java new file mode 100644 index 0000000..241d970 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/LikeEntity.java @@ -0,0 +1,21 @@ +package com.project.mutsa_sns.entity; + +import jakarta.persistence.*; +import lombok.Data; + +@Entity +@Data +public class LikeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 좋아요 아이디 + + @ManyToOne + @JoinColumn(name = "article_id") + private ArticleEntity article; // 좋아요가 속한 피드 정보 + + @ManyToOne + @JoinColumn(name = "user_id") + private UserEntity user; // 좋아요를 누른 사용자 정보 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/UserEntity.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/UserEntity.java new file mode 100644 index 0000000..ca91144 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/entity/UserEntity.java @@ -0,0 +1,25 @@ +package com.project.mutsa_sns.entity; + +import jakarta.persistence.*; +import lombok.Data; + +@Data +@Entity +@Table(name = "users") +public class UserEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 사용자의 ID + + @Column(nullable = false, unique = true) + private String username; // 사용자의 이름 (아이디) + + @Column(nullable = false) + private String password; // 사용자의 암호화된 비밀번호 + + private String profile_img; // 사용자의 프로필 이미지 경로 + + private String email; // 사용자의 이메일 + + private String phone; // 사용자의 전화번호 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/jwt/JwtRequestDto.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/jwt/JwtRequestDto.java new file mode 100644 index 0000000..5a3393a --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/jwt/JwtRequestDto.java @@ -0,0 +1,9 @@ +package com.project.mutsa_sns.jwt; + +import lombok.Data; + +@Data +public class JwtRequestDto { + private String username; // 사용자명 + private String password; // 비밀번호 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/jwt/JwtTokenDto.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/jwt/JwtTokenDto.java new file mode 100644 index 0000000..dbc0d5d --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/jwt/JwtTokenDto.java @@ -0,0 +1,8 @@ +package com.project.mutsa_sns.jwt; + +import lombok.Data; + +@Data +public class JwtTokenDto { + private String token; // JWT 토큰 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/jwt/JwtTokenFilter.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/jwt/JwtTokenFilter.java new file mode 100644 index 0000000..e3e85d3 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/jwt/JwtTokenFilter.java @@ -0,0 +1,66 @@ +package com.project.mutsa_sns.jwt; + +import com.project.mutsa_sns.detail.CustomUserDetails; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.ArrayList; + +@Slf4j +@Component +public class JwtTokenFilter extends OncePerRequestFilter { + // OncePerRequestFilter : 요청이 들어올 때마다 한 번만 실행 + private final JwtTokenUtils jwtTokenUtils; + + public JwtTokenFilter(JwtTokenUtils jwtTokenUtils) { + this.jwtTokenUtils = jwtTokenUtils; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + // 헤더에서 인증 토큰을 추출 + String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + // 실제 JWT 토큰만 추출 + String token = authHeader.split(" ")[1]; + // JWT 유효성 검사 + if (jwtTokenUtils.validate(token)) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + // JWT 에서 사용자 이름을 가져옴 + String username = jwtTokenUtils + .parseClaims(token) + .getSubject(); + // 인증 토큰 생성 + AbstractAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + CustomUserDetails.builder() + .username(username) + .build(), token, new ArrayList<>() + ); + // SecurityContext 에 사용자 정보 설정 + context.setAuthentication(authenticationToken); + // SecurityContextHolder 에 SecurityContext 설정 + SecurityContextHolder.setContext(context); + log.info("JWT 로 인증 정보 설정 완료"); + } else { + log.warn("JWT 유효성 검사 실패"); + } + } + // 다음 필터 또는 요청 핸들러로 이동 + filterChain.doFilter(request, response); + } +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/jwt/JwtTokenUtils.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/jwt/JwtTokenUtils.java new file mode 100644 index 0000000..56fc373 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/jwt/JwtTokenUtils.java @@ -0,0 +1,66 @@ +package com.project.mutsa_sns.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.time.Instant; +import java.util.Date; + +@Slf4j +@Component +public class JwtTokenUtils { + private final Key signingKey; + private final JwtParser jwtParser; + + public JwtTokenUtils( + @Value("${jwt.secret}") + String jwtSecret + ) { + // JWT 서명에 사용할 키를 생성 + this.signingKey = Keys.hmacShaKeyFor(jwtSecret.getBytes()); + // JWT 파서를 생성하여 서명 키로 설정 + this.jwtParser = Jwts + .parserBuilder() + .setSigningKey(this.signingKey) + .build(); + } + + // JWT 가 유효한지 판단하는 메서드 + public boolean validate(String token) { + try { + // 암호화된 JWT 를 해석하기 위한 메서드 + jwtParser.parseClaimsJws(token); + return true; + } catch (Exception e) { + log.warn("invalid jwt: {}", e.getClass()); + return false; + } + } + + // JWT 를 해석해서 사용자 정보를 회수하는 메서드 + public Claims parseClaims(String token) { + return jwtParser + .parseClaimsJws(token) + .getBody(); + } + + // 주어진 사용자 정보를 바탕으로 JWT 를 문자열로 생성하는 메서드 + public String generateToken(UserDetails userDetails) { + // Claims : JWT 에 담기는 정보의 단위 + Claims jwtClaims = Jwts.claims() + .setSubject(userDetails.getUsername()) + .setIssuedAt(Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().plusSeconds(3600))); + return Jwts.builder() + .setClaims(jwtClaims) + .signWith(signingKey) + .compact(); + } +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/ArticleImageRepository.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/ArticleImageRepository.java new file mode 100644 index 0000000..8acccdf --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/ArticleImageRepository.java @@ -0,0 +1,12 @@ +package com.project.mutsa_sns.repository; + +import com.project.mutsa_sns.entity.ArticleEntity; +import com.project.mutsa_sns.entity.ArticleImageEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ArticleImageRepository extends JpaRepository { + + List findByArticle(ArticleEntity article); // 특정 게시물에 속한 이미지 조회 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/ArticleRepository.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/ArticleRepository.java new file mode 100644 index 0000000..8de1507 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/ArticleRepository.java @@ -0,0 +1,8 @@ +package com.project.mutsa_sns.repository; + +import com.project.mutsa_sns.entity.ArticleEntity; +import org.springframework.data.jpa.repository.JpaRepository; + + +public interface ArticleRepository extends JpaRepository { +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/CommentRepository.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/CommentRepository.java new file mode 100644 index 0000000..9ca09db --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/CommentRepository.java @@ -0,0 +1,8 @@ +package com.project.mutsa_sns.repository; + +import com.project.mutsa_sns.entity.CommentEntity; +import org.springframework.data.jpa.repository.JpaRepository; + + +public interface CommentRepository extends JpaRepository { +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/LikeRepository.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/LikeRepository.java new file mode 100644 index 0000000..0de1d07 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/LikeRepository.java @@ -0,0 +1,12 @@ +package com.project.mutsa_sns.repository; + +import com.project.mutsa_sns.entity.ArticleEntity; +import com.project.mutsa_sns.entity.LikeEntity; +import com.project.mutsa_sns.entity.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface LikeRepository extends JpaRepository { + Optional findByArticleAndUser(ArticleEntity article, UserEntity user); +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/UserRepository.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/UserRepository.java new file mode 100644 index 0000000..1a5097d --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/repository/UserRepository.java @@ -0,0 +1,11 @@ +package com.project.mutsa_sns.repository; + +import com.project.mutsa_sns.entity.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); // 사용자명으로 사용자 정보 조회 + Boolean existsByUsername(String username); // 사용자명으로 사용자 존재 여부 확인 +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/ArticleService.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/ArticleService.java new file mode 100644 index 0000000..289775e --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/ArticleService.java @@ -0,0 +1,213 @@ +package com.project.mutsa_sns.service; + +import com.project.mutsa_sns.dto.ArticleDetailResponseDto; +import com.project.mutsa_sns.dto.ArticleListResponseDto; +import com.project.mutsa_sns.dto.CommentResponseDto; +import com.project.mutsa_sns.dto.ResponseDto; +import com.project.mutsa_sns.entity.ArticleEntity; +import com.project.mutsa_sns.entity.ArticleImageEntity; +import com.project.mutsa_sns.entity.CommentEntity; +import com.project.mutsa_sns.entity.UserEntity; +import com.project.mutsa_sns.repository.ArticleImageRepository; +import com.project.mutsa_sns.repository.ArticleRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ArticleService { + private final ArticleRepository articleRepository; + private final ArticleImageRepository articleImageRepository; + private final AuthService authService; + + // 피드 생성 메서드 + public ResponseDto createArticle(String title, String content, + MultipartFile representativeImage, List images) { + + // 현재 로그인한 사용자 정보를 가져옴 + UserEntity userEntity = authService.getUser(); + + // 피드 엔티티 생성 및 저장 + ArticleEntity articleEntity = new ArticleEntity(); + articleEntity.setUser(userEntity); + articleEntity.setTitle(title); + articleEntity.setContent(content); + articleEntity.setDraft(false); + + articleRepository.save(articleEntity); + + // 대표 이미지 처리 + handleArticleImage(articleEntity, representativeImage, true); + + // 추가 이미지 처리 + for (MultipartFile image : images) { + handleArticleImage(articleEntity, image, false); + } + + return new ResponseDto("피드가 생성되었습니다."); + } + + // 피드 목록 조회 기능 + public List getArticleList() { + List articles = articleRepository.findAll(); + List responseDtoList = new ArrayList<>(); + + for (ArticleEntity article : articles) { + ArticleListResponseDto responseDto = new ArticleListResponseDto(); + responseDto.setId(article.getId()); + responseDto.setUserId(article.getUser().getId()); + responseDto.setUsername(article.getUser().getUsername()); + responseDto.setTitle(article.getTitle()); + + // 피드에 첨부된 이미지가 있다면 첫번째 이미지 URL을, 없다면 기본 이미지 URL을 설정 + List images = article.getArticleImages(); + if (!images.isEmpty()) { + responseDto.setImageUrl(images.get(0).getImageUrl()); + } else { + responseDto.setImageUrl("/static/media/**"); + } + + responseDtoList.add(responseDto); + } + + return responseDtoList; + } + + // 피드 단독 조회 기능 + public ArticleDetailResponseDto getArticleDetail(Long articleId) { + ArticleEntity article = articleRepository.findById(articleId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + + ArticleDetailResponseDto responseDto = new ArticleDetailResponseDto(); + responseDto.setId(article.getId()); + responseDto.setUserId(article.getUser().getId()); + responseDto.setUsername(article.getUser().getUsername()); + responseDto.setTitle(article.getTitle()); + responseDto.setContent(article.getContent()); + responseDto.setDeletedAt(article.getDeletedAt()); + + List imageUrl = new ArrayList<>(); + for (ArticleImageEntity image : article.getArticleImages()) { + imageUrl.add(image.getImageUrl()); + } + responseDto.setImageUrl(imageUrl); + + // 댓글, 좋아요? + // 댓글 정보 추가 + List commentResponseList = new ArrayList<>(); + for (CommentEntity comment : article.getComments()) { + CommentResponseDto commentResponseDto = new CommentResponseDto(); + commentResponseDto.setId(comment.getId()); + commentResponseDto.setUserId(comment.getUser().getId()); + commentResponseDto.setUsername(comment.getUser().getUsername()); + commentResponseDto.setContent(comment.getContent()); + commentResponseDto.setDeletedAt(comment.getDeletedAt()); + commentResponseList.add(commentResponseDto); + } + responseDto.setComments(commentResponseList); + + return responseDto; + } + + // 피드 업데이트 메서드 + public ResponseDto updateArticle(Long articleId, String title, String content, + MultipartFile representativeImage, List images) { + + UserEntity userEntity = authService.getUser(); + + ArticleEntity articleEntity = articleRepository.findById(articleId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + + if (!articleEntity.getUser().getId().equals(userEntity.getId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "해당 피드를 수정할 권한이 없습니다."); + } + + articleEntity.setTitle(title); + articleEntity.setContent(content); + + articleRepository.save(articleEntity); + + // 기존 이미지들을 삭제 + List existingImages = articleImageRepository.findByArticle(articleEntity); + // 실제 파일도 삭제 처리할 수 있음 + articleImageRepository.deleteAll(existingImages); + + // 대표 이미지 및 추가 이미지 처리 + handleArticleImage(articleEntity, representativeImage, true); + for (MultipartFile image : images) { + handleArticleImage(articleEntity, image, false); + } + + return new ResponseDto("피드가 업데이트되었습니다."); + } + + // 피드 삭제 기능 + public ResponseDto deleteArticle(Long articleId) { + UserEntity userEntity = authService.getUser(); + + ArticleEntity article = articleRepository.findById(articleId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + + if (!article.getUser().getId().equals(userEntity.getId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "게시물을 삭제할 권한이 없습니다."); + } + + // 게시물 삭제 대신 삭제되었다는 표시 (deletedAt 필드에 현재 시간 설정) + article.setDeletedAt(LocalDateTime.now()); + article = articleRepository.save(article); + log.info(String.valueOf(article.getDeletedAt())); + + return new ResponseDto("게시물이 삭제되었습니다."); + } + + // 이미지 처리 메서드 + public void handleArticleImage(ArticleEntity articleEntity, MultipartFile image, boolean isRepresentative) { + // 이미지를 저장할 디렉토리 경로 설정 + String imageDir = String.format("media/article/%d/", articleEntity.getId()); + + try { + // 이미지 저장 디렉토리 생성 + Files.createDirectories(Path.of(imageDir)); + } catch (IOException ex) { + log.error("이미지 디렉토리 생성 실패: {}", ex.getMessage()); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR); + } + + // 이미지 파일 이름 분리 및 확장자 추출 + String originalFilename = image.getOriginalFilename(); + String[] fileNameSplit = originalFilename.split("\\."); + String extension = fileNameSplit[fileNameSplit.length - 1]; + String imageName = isRepresentative ? "representative_image." + extension : UUID.randomUUID().toString() + "." + extension; + String imagePath = imageDir + imageName; + + log.info(imagePath); + + // 이미지 파일을 실제 경로에 저장 + try { + image.transferTo(Path.of(imagePath)); + } catch (IOException ex) { + log.error("이미지 저장 실패: {}", ex.getMessage()); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR); + } + + // 피드 이미지 엔티티 생성 및 저장 + ArticleImageEntity articleImageEntity = new ArticleImageEntity(); + articleImageEntity.setImageUrl(String.format("/static/%d/%s", articleEntity.getId(), imageName)); + articleImageEntity.setArticle(articleEntity); + + articleImageRepository.save(articleImageEntity); + } +} \ No newline at end of file diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/AuthService.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/AuthService.java new file mode 100644 index 0000000..ebf2884 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/AuthService.java @@ -0,0 +1,45 @@ +package com.project.mutsa_sns.service; + +import com.project.mutsa_sns.entity.UserEntity; +import com.project.mutsa_sns.jwt.JwtTokenUtils; +import com.project.mutsa_sns.repository.UserRepository; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +@Service +public class AuthService { + private final HttpServletRequest request; + private final JwtTokenUtils jwtTokenUtils; + private final UserRepository userRepository; + + public AuthService(HttpServletRequest request, JwtTokenUtils jwtTokenUtils, UserRepository userRepository) { + this.request = request; + this.jwtTokenUtils = jwtTokenUtils; + this.userRepository = userRepository; + } + + // 현재 로그인한 사용자 정보 가져오기 + public UserEntity getUser() { + // Authorization 헤더에서 토큰 추출 + String token = extractTokenFromHeader(request.getHeader(HttpHeaders.AUTHORIZATION)); + if (!jwtTokenUtils.validate(token)) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + // 토큰을 해석하여 사용자명 추출 + String username = jwtTokenUtils.parseClaims(token).getSubject(); + return userRepository.findByUsername(username) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + } + + // 헤더에서 토큰 추출 + private String extractTokenFromHeader(String authHeader) { + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + } + return authHeader.split(" ")[1]; + } +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/CommentService.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/CommentService.java new file mode 100644 index 0000000..8985b1b --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/CommentService.java @@ -0,0 +1,88 @@ +package com.project.mutsa_sns.service; + +import com.project.mutsa_sns.dto.CommentRequestDto; +import com.project.mutsa_sns.dto.ResponseDto; +import com.project.mutsa_sns.entity.ArticleEntity; +import com.project.mutsa_sns.entity.CommentEntity; +import com.project.mutsa_sns.entity.UserEntity; +import com.project.mutsa_sns.repository.ArticleRepository; +import com.project.mutsa_sns.repository.CommentRepository; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; + +@Service +public class CommentService { + + private final CommentRepository commentRepository; + private final AuthService authService; + private final ArticleRepository articleRepository; // ArticleRepository 추가 + + public CommentService(CommentRepository commentRepository, AuthService authService, ArticleRepository articleRepository) { + this.commentRepository = commentRepository; + this.authService = authService; + this.articleRepository = articleRepository; // ArticleRepository 초기화 + } + + // 댓글 작성 + public ResponseDto createComment(Long articleId, CommentRequestDto requestDto) { + // 현재 로그인한 사용자 정보 가져오기 + UserEntity userEntity = authService.getUser(); + + // 해당 ArticleEntity 가져오기 + ArticleEntity articleEntity = articleRepository.findById(articleId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "해당하는 Article을 찾을 수 없습니다.")); + + // 댓글 엔티티 생성 및 저장 + CommentEntity commentEntity = new CommentEntity(); + commentEntity.setUser(userEntity); + commentEntity.setArticle(articleEntity); + commentEntity.setContent(requestDto.getContent()); + + commentRepository.save(commentEntity); + + return new ResponseDto("댓글이 작성되었습니다."); + } + + // 댓글 수정 + public ResponseDto updateComment(Long commentId, CommentRequestDto requestDto) { + // 현재 로그인한 사용자 정보 가져오기 + UserEntity userEntity = authService.getUser(); + + // 댓글 조회 + CommentEntity commentEntity = commentRepository.findById(commentId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "해당하는 댓글을 찾을 수 없습니다.")); + + // 작성자 확인 및 수정 + if (!commentEntity.getUser().getId().equals(userEntity.getId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "댓글을 수정할 권한이 없습니다."); + } + + commentEntity.setContent(requestDto.getContent()); + commentRepository.save(commentEntity); + + return new ResponseDto("댓글이 수정되었습니다."); + } + + // 댓글 삭제 + public ResponseDto deleteComment(Long commentId) { + // 현재 로그인한 사용자 정보 가져오기 + UserEntity userEntity = authService.getUser(); + + // 댓글 조회 + CommentEntity commentEntity = commentRepository.findById(commentId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "해당하는 댓글을 찾을 수 없습니다.")); + + // 작성자 확인 및 삭제 표시 + if (!commentEntity.getUser().getId().equals(userEntity.getId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "댓글을 삭제할 권한이 없습니다."); + } + + commentEntity.setDeletedAt(LocalDateTime.now()); + commentRepository.save(commentEntity); + + return new ResponseDto("댓글이 삭제되었습니다."); + } +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/LikeService.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/LikeService.java new file mode 100644 index 0000000..68df269 --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/LikeService.java @@ -0,0 +1,50 @@ +package com.project.mutsa_sns.service; + +import com.project.mutsa_sns.dto.LikeResponseDto; +import com.project.mutsa_sns.entity.ArticleEntity; +import com.project.mutsa_sns.entity.UserEntity; +import com.project.mutsa_sns.repository.ArticleRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +@Service +@RequiredArgsConstructor +public class LikeService { + + private final ArticleRepository articleRepository; + private final AuthService authService; + + // 피드 좋아요 토글 기능 + public LikeResponseDto toggleArticleLike(Long articleId) { + // 피드 정보 가져오기 + ArticleEntity article = articleRepository.findById(articleId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + + // 현재 로그인한 사용자 정보 가져오기 + UserEntity currentUser = authService.getUser(); + + // 자신의 피드인 경우 좋아요 할 수 없음 + if (currentUser.getId().equals(article.getUser().getId())) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "자신의 피드는 좋아요를 할 수 없습니다."); + } + + // 이미 좋아요를 했는지 확인 + boolean isLiked = article.getLikes().contains(currentUser); + + // 이미 좋아요를 했다면 좋아요 취소, 아니면 좋아요 추가 + if (isLiked) { + article.getLikes().remove(currentUser); + } else { + article.getLikes().add(currentUser); + } + + // 변경된 정보 저장 + articleRepository.save(article); + + // 응답 메시지 생성 + String message = isLiked ? "좋아요가 취소되었습니다." : "좋아요가 추가되었습니다."; + return new LikeResponseDto(true, message); + } +} diff --git a/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/UserService.java b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/UserService.java new file mode 100644 index 0000000..8e933ce --- /dev/null +++ b/mutsa_sns_review/src/main/java/com/project/mutsa_sns/service/UserService.java @@ -0,0 +1,106 @@ +package com.project.mutsa_sns.service; + +import com.project.mutsa_sns.detail.CustomUserDetails; +import com.project.mutsa_sns.detail.JpaUserDetailsManager; +import com.project.mutsa_sns.dto.LoginRequestDto; +import com.project.mutsa_sns.dto.RegisterRequestDto; +import com.project.mutsa_sns.dto.ResponseDto; +import com.project.mutsa_sns.entity.UserEntity; +import com.project.mutsa_sns.jwt.JwtTokenDto; +import com.project.mutsa_sns.jwt.JwtTokenUtils; +import com.project.mutsa_sns.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserService { + private final UserRepository userRepository; + private final JwtTokenUtils jwtTokenUtils; + private final PasswordEncoder passwordEncoder; + private final JpaUserDetailsManager manager; + private final AuthService authService; + + // 로그인 기능 + public JwtTokenDto loginUser(LoginRequestDto loginRequestDto) { + String username = loginRequestDto.getUsername(); + + if (!manager.userExists(username)) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "올바른 Username 이 아닙니다"); + } + + UserDetails userDetails = manager.loadUserByUsername(username); + + if (!passwordEncoder.matches(loginRequestDto.getPassword(), userDetails.getPassword())) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Password 가 일치하지 않습니다"); + } + + JwtTokenDto tokenDto = new JwtTokenDto(); + tokenDto.setToken(jwtTokenUtils.generateToken(userDetails)); + return tokenDto; + } + + // 회원가입 기능 + public ResponseDto registerUser(RegisterRequestDto registerRequestDto) { + String username = registerRequestDto.getUsername(); + String password = registerRequestDto.getPassword(); + + if (manager.userExists(username)) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Username 이 이미 존재합니다"); + } + + CustomUserDetails user = CustomUserDetails.builder() + .username(username) + .password(passwordEncoder.encode(password)) + .email(registerRequestDto.getEmail()) + .phone(registerRequestDto.getPhone()) + .build(); + + manager.createUser(user); + + return new ResponseDto("회원가입을 성공했습니다"); + } + + // 프로필 이미지 업로드 기능 + public ResponseDto uploadProfileImage(MultipartFile multipartFile) { + UserEntity userEntity = authService.getUser(); + + String profileDir = String.format("media/%d/", userEntity.getId()); + + try { + Files.createDirectories(Path.of(profileDir)); + } catch (IOException e) { + log.error(e.getMessage()); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR); + } + + String originalFilename = multipartFile.getOriginalFilename(); + String[] fileNameSplit = originalFilename.split("\\."); + String extension = fileNameSplit[fileNameSplit.length - 1]; + String profileFilename = "image." + extension; + String profilePath = profileDir + profileFilename; + + try { + multipartFile.transferTo(Path.of(profilePath)); + } catch (IOException e) { + log.error(e.getMessage()); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR); + } + + userEntity.setProfile_img(profilePath); + userRepository.save(userEntity); + + return new ResponseDto("이미지가 등록되었습니다."); + } +} diff --git a/mutsa_sns_review/src/main/resources/application.yaml b/mutsa_sns_review/src/main/resources/application.yaml new file mode 100644 index 0000000..fe63fd6 --- /dev/null +++ b/mutsa_sns_review/src/main/resources/application.yaml @@ -0,0 +1,27 @@ +spring: + datasource: + url: jdbc:sqlite:db.sqlite + driver-class-name: org.sqlite.JDBC + jpa: + hibernate: + ddl-auto: create + show-sql: true + database-platform: org.hibernate.community.dialect.SQLiteDialect + defer-datasource-initialization: true + sql: + init: + mode: always + + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + mvc: + # /static/ 으로 시작하는 요청에 대해 정적 파일 서빙 + static-path-pattern: /static/** + web: + resources: + # 정적 파일 탐색 장소 + static-locations: file:media/,classpath:/static +jwt: + secret: JL3wop2XntqN57XU7aWmSYb2btfSz12EgCjDYnFb4GN4BPH6mXxMwKQmoZP \ No newline at end of file diff --git a/mutsa_sns_review/src/test/java/com/project/mutsa_sns/MutsaSnsApplicationTests.java b/mutsa_sns_review/src/test/java/com/project/mutsa_sns/MutsaSnsApplicationTests.java new file mode 100644 index 0000000..2111908 --- /dev/null +++ b/mutsa_sns_review/src/test/java/com/project/mutsa_sns/MutsaSnsApplicationTests.java @@ -0,0 +1,13 @@ +package com.project.mutsa_sns; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class MutsaSnsApplicationTests { + + @Test + void contextLoads() { + } + +}