11. Response 코드 및 메시지 인터페이스 작성

 

1. Http Status Code & Message 설명

200
- 성공 : "SU" / "Success."

400
- 유효성 검증 실패 : "VF" / "Validation failed."
- 중복된 이메일 : "DE" / "Duplicated Email."
- 중복된 전화번호 : "DT" / "Duplicate tel_number."
- 중복된 닉네임 : "DN" / "Duplicate nickname."
- 존재하지 않는 유저 : "NU" / "This user does not exist."
- 존재하지 않는 게시물 : "NB" / "This board does not exist."

401
- 로그인 실패 : "SF" / "Login information mismatch."
- 인증 실패 : "AF" / "Authorization Failed."

403
- 권한 없음 : "NP" / "Do not have permission."

500
- 데이터베이스 실패 : "DBE" / "Database Error."

 

 

2. ResponseCode.java 작성  (board-backend / src / main / java 하위)

package kr.co.sorin.board_backend.common;

public interface ResponseCode {

    // interface 필드는 무조건 public static final로 해야함, 생략해도 public static final로 인식함

    // HTTP STATUS 200
    String SUCCESS = "SU";
    // HTTP STATUS 400
    String VALIDATION_FAILED = "VF";
    String DUPLICATE_EMAIL = "DE";
    String DUPLICATE_TEL_NUMBER = "DT";
    String DUPLICATE_NICKNAME = "DN";
    String NOT_EXISTED_USER = "NU";
    String NOT_EXISTED_BOARD = "NB";
    // HTTP STATUS 401
    String SIGN_IN_FAILED = "SF";
    String AUTHORIZATION_FAILED = "AF";
    // HTTP STATUS 403
    String NO_PERMISSION = "NP";
    // HTTP STATUS 500
    String DATABASE_ERROR = "DBE";

}

 

 

3. ResponseMessage.java 작성

package kr.co.sorin.board_backend.common;

public interface ResponseMessage {
    // HTTP STATUS 200
    String SUCCESS = "Success.";
    // HTTP STATUS 400
    String VALIDATION_FAILED = "Validation failed.";
    String DUPLICATE_EMAIL = "Duplicated Email.";
    String DUPLICATE_TEL_NUMBER = "Duplicate tel_number.";
    String DUPLICATE_NICKNAME = "Duplicate nickname.";
    String NOT_EXISTED_USER = "This user does not exist.";
    String NOT_EXISTED_BOARD = "This board does not exist.";
    // HTTP STATUS 401
    String SIGN_IN_FAILED = "Login information mismatch.";
    String AUTHORIZATION_FAILED = "Authorization Failed.";
    // HTTP STATUS 403
    String NO_PERMISSION = "Do not have permission.";
    // HTTP STATUS 500
    String DATABASE_ERROR = "Database Error.";
}

 

 

4. WebSecurityConfig.java 수정

class FailedAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.getWriter().write("{ \"code\": \"NP\", \"message\": \"Do not have Permission.\" }");
    }
}

-->

class FailedAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write("{ \"code\": \"AF\", \"message\": \"Do not have Permission.\" }");
    }
}

 

5. ResponseDto.java 작성

package kr.co.sorin.board_backend.dto.response;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import kr.co.sorin.board_backend.common.ResponseCode;
import kr.co.sorin.board_backend.common.ResponseMessage;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class ResponseDto {

    private String code;
    private String message;

    // 상속 및 확장하여 사용
    public static ResponseEntity<ResponseDto> databaseError() {
        ResponseDto responseBody = new ResponseDto(ResponseCode.DATABASE_ERROR, ResponseMessage.DATABASE_ERROR);

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(responseBody);
    }

}

 

 

6. board-frontend / src / tsconfig.json 수정 : "baseUrl": "./src", 추가

{
  "compilerOptions": {
    "target": "es5",
    "baseUrl": "./src",
    "lib": ["dom", "dom.iterable", "esnext"],
    
    ...
    
    }
}

 

7. board-frontend / src / apis / response / index.ts 작성

import ResponseDto from "./Response.dto";

export type { ResponseDto }; // interface는 내보낼 때, export 다음에, type을 써줘야함

 

8. board-frontend / src / apis / response / Response.dto.ts 작성

import { ResponseCode } from "types/enum";

export default interface ResponseDto {
  code: ResponseCode;
  message: string;
}

 

9. board-frontend / src / types / enum / index.ts 작성

import ResponseCode from "./response-code.enum";

export { ResponseCode }; // enum은 export 다음에 type 안써줘도 됨

 

10. board-frontend / src / types / enum / response-code.enum.ts 작성

enum ResponseCode {
  // HTTP STATUS 200
  SUCCESS = "SU",
  VALIDATION_FAILED = "VF",
  DUPLICATE_EMAIL = "DE",
  DUPLICATE_TEL_NUMBER = "DT",
  DUPLICATE_NICKNAME = "DN",
  NOT_EXISTED_USER = "NU",
  NOT_EXISTED_BOARD = "NB",
  // HTTP STATUS 401
  SIGN_IN_FAILED = "SF",
  AUTHORIZATION_FAILED = "AF",
  // HTTP STATUS 403
  NO_PERMISSION = "NP",
  // HTTP STATUS 500
  DATABASE_ERROR = "DBE",
}

export default ResponseCode;

 

 

출처 : https://www.youtube.com/@jiraynorprogramming1589

10. Spring Security (Filter, Config)

 

1. Spring 3.x 특징

-- Spring 3.x 버전 특징
구 -> 신

1. Java 17 이상 사용
2. javax.* -> jakarta.*
3. application.properties / application.yaml (use-legacy-processing)
4. AntPathMatcher -> PathPatternParser
5. 외부 라이브러리 사용 시, Jakarta, Spring Framework 6.x 버전 지원 여부를 확인해야 함

 

2. import javax.* -> jakarta.* 로 모두 수정

 

3. build.gradle 수정

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.3'
	id 'io.spring.dependency-management' version '1.1.6'
}

group = 'kr.co.sorin'
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'

	implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'

	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

4. WebSecurityConfig.java 수정

package kr.co.sorin.board_backend.config;

import java.io.IOException;

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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import kr.co.sorin.board_backend.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    protected SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception {
        // spring 3.x
        httpSecurity
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(CsrfConfigurer::disable)
            .httpBasic(HttpBasicConfigurer::disable)
            .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(request -> request
                .requestMatchers("/", "/api/v1/auth/**", "/api/v1/search/**", "/file/**").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/v1/board/**", "/api/v1/user/*").permitAll()
                .anyRequest().authenticated()
            )
            .exceptionHandling(exceptionHandling -> exceptionHandling.authenticationEntryPoint(new FailedAuthenticationEntryPoint()))
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return httpSecurity.build();
    }

    @Bean
    protected CorsConfigurationSource corsConfigurationSource() {

        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedOrigin("*");
        configuration.addAllowedMethod("*");
        configuration.addExposedHeader("*");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration); // configuration 등록

        return source;
    }
}

class FailedAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.getWriter().write("{ \"code\": \"NP\", \"message\": \"Do not have Permission.\" }");
    }
}

 

5. CorsConfig.java 삭제

  - WebSecurityConfig.java로 설정을 모두 옮겼기 때문

 

 

출처 : https://www.youtube.com/@jiraynorprogramming1589

1. react 프로젝트 생성 (board-frontend)

  - npx create-app react --template typescript

  - App.tsx 초기화

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  - index.tsx 초기화

 

  - src 하위 폴더 구조 생성

 

 

2. Authentication 방식

1. Basic Authentication
- 사용자 이름 / 비밀번호를 Base64로 인코딩하여 Authorizaiton 헤더에 포함하여 전송
- 매우 안전하지 않음, SSL/TLS와 함께 사용됨

Ex: Authorization: Basic ~~~

2. Bearer Token Authentication
- 헤더에 토큰을 포함하여 전송 Authorization 헤더에 포함하여 전송
- JWT을 사용하여 인증
- 간단한 방식, 상태를 유지하지 않음, 확장성이 높음
- 단점 : 토큰 노출 위험, 토큰 관리가 힘듬, 귀찮음

Ex: Authorizaiton: Bearer ~~~~~

3. OAuth 방식
- 토큰기반 인증 방식, 사용자가 직접 자격을 증명 X 미리 인증 받아서 토큰을 발급 받고
- 이 토큰을 이용하여 API를 요청하는 방식, OAuth 2.0

Ex: Kakao, Naver, Git, Facebook login ..

4. API Key 방식
- 발급 받은 키를 요청

5. Session based Authentication
- Session ID를 생성하여 세션이나 쿠키에 포함하여 인증

- JWT (JSON Web Token) : 클레임이라고 불리는 정보를 JSON 형태로 안전하게 전송하기 위한 토큰
- 인증과 정보 교환에 사용, 서명이 되어 있어서 신뢰성 확보가 가능

1. Header : 토큰의 타입과 사용된 알고리즘 정보를 담고 있음, Base64 URL로 인코딩
2. Payload : 클레임 정보, 대상, 발행자, 만료 시간 등 다양한 정보가 포함, Base64 URL로 인코딩
3. Signature : Header와 Payload, Secret Key를 사용하여 생성된 서명

인증, 정보교환

장점 :
상태유지하지 않는다. = stateless, 서버가 클라이언트의 상태를 유지안한다
간단하고 자기 포함적
확장성이 높다, 토큰을 만들어 놓으면 여러 시스템에서 사용 가능

단점 : 
크기 : 클레임이 많을수록 토큰의 크기가 커짐
보안 : 서명은 되어있지만, 암호화는 되어있지 않음, 중요한 정보를 JWT에 포함할 수 없음
토큰 관리 : 만료 시간, 갱신 잘해줘야 함

 

 

3. Spring Initializr로 Gradle Project 생성 (Gradle 2.7.14)

 

  - src/main/java/test 하위 삭제및 src/main/java 하위 폴더 구조 잡기

 

  - applicaiton.properties 설정

 

  - JwtProvider.java 생성

package kr.co.sorin.board_backend.provider;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

@Component
public class JwtProvider {

    private String secretKey = "S3cr3tK3y";

    public String create(String email) {
        Date expiredDate = Date.from(Instant.now().plus(1, ChronoUnit.HOURS));

        String jwt = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, secretKey) // 수정: SignatureAlgorithm.ES256 ->
                .setSubject(email)
                .setIssuedAt(new Date())
                .setExpiration(expiredDate)
                .compact();

        return jwt;
    }

    public String validate(String jwt) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(jwt)
                    .getBody();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }

        return claims.getSubject();
    }
}

 

  - WebSecurityConfig.java 생성

package kr.co.sorin.board_backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

import kr.co.sorin.board_backend.filter.JWTAuthenticationFilter;
import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JWTAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    protected SecurityFilterChain configure(HttpSecurity HttpSecurity) throws Exception {
        HttpSecurity
                .cors().and()
                .csrf().disable()
                .httpBasic().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                .anyRequest().permitAll();

    }
}

 

  - JwtAuthenticationFilter.java 생성

package kr.co.sorin.board_backend.filter;

import java.io.IOException;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import kr.co.sorin.board_backend.provider.JwtProvider;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtProvider jwtProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String token = parseBearerToken(request);

        try {
            if (token == null) {
                filterChain.doFilter(request, response);
                return;
            }

            String email = jwtProvider.validate(token);

            if (email == null) {
                filterChain.doFilter(request, response);
                return;
            }

            // context에 등록
            AbstractAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, null,
                    AuthorityUtils.NO_AUTHORITIES);

            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 웹 인증정보 세부정보
                                                                                                        // 셋팅

            SecurityContext SecurityContext = SecurityContextHolder.createEmptyContext();
            SecurityContext.setAuthentication(authenticationToken);

            SecurityContextHolder.setContext(SecurityContext); // 외부에서 사용 가능
        } catch (Exception e) {
            e.printStackTrace();
        }

        filterChain.doFilter(request, response); // 다음 필터로 request를 넘긴다.

    }

    private String parseBearerToken(HttpServletRequest request) {

        String authorization = request.getHeader("Authorization");

        boolean hasAuthorization = StringUtils.hasText(authorization);

        if (!hasAuthorization)
            return null;

        boolean isBearer = authorization.startsWith("Bearer ");
        if (!isBearer)
            return null;

        String token = authorization.substring(7);

        return token;

    }

}

 

  - WebSecurityConfig.java 작성

package kr.co.sorin.board_backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

import kr.co.sorin.board_backend.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    protected SecurityFilterChain configure(HttpSecurity HttpSecurity) throws Exception {
        HttpSecurity
                .cors().and()
                .csrf().disable()
                .httpBasic().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                .anyRequest().permitAll();

    }
}

 

- CorsConfig.java 작성

package kr.co.sorin.board_backend.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings (CorsRegistry corsRegistry) {
        corsRegistry
            .addMapping("/**")
            .allowedMethods("*")
            .allowedOrigins("*");
    }

}

 

 

출처 : https://www.youtube.com/@jiraynorprogramming1589

1. DDL 작성하기 (게시글 조회 VIEW)

CREATE OR REPLACE VIEW board_list_view AS
SELECT
    B.board_number AS board_number
  , B.title AS title
  , B.content AS content
  , I.image AS image
  , B.favorite_count AS favorite_count
  , B.comment_count AS comment_count
  , B.view_count AS view_count
  , B.write_datetime AS write_datetime
  , B.writer_email AS writer_email
  , U.nickname AS writer_nickname
  , U.profile_image AS writer_profile_image
FROM board AS B
INNER JOIN user AS U
ON B.writer_email = U.email
LEFT JOIN (SELECT board_number, ANY_VALUE(image) AS image FROM image GROUP BY board_number) AS I
ON B.board_number = I.board_number;

 

2. API 기반 DML 작성하기

-- Active: 1725368211487@@localhost@3306@board
-- 회원가입
INSERT INTO user VALUES ('email@email.com', 'P!ssw0rd', 'nickname', '01012345678', '서울특별시 종로구', '그랑서울', null);

-- 로그인
SELECT * FROM user WHERE email = 'email@email.com';

-- 게시물 작성
INSERT INTO board (title, content, write_datetime, favorite_count, comment_count, view_count, writer_email)
VALUES ('제목', '내용', '2024-09-06 23:25:54', 0, 0, 0, 'email@email.com');

INSERT INTO image
VALUES (2, 'url');

-- 댓글 작성
INSERT INTO comment (content, write_datetime, user_email, board_number)
VALUES ('반갑습니다', '2024-09-06 23:28:50', 'email@email.com', 3);

UPDATE board
SET comment_count = comment_count + 1
WHERE board_number = 2;

-- 좋아요 등록, 취소
INSERT INTO favorite
VALUES ('email@email.com', 2);

UPDATE board
SET favorite_count = favorite_count + 1
WHERE board_number = 2;

DELETE FROM favorite
WHERE user_email = 'email@email.com'
AND board_number = 2;

UPDATE board
SET favorite_count = favorite_count - 1
WHERE board_number = 2;

-- 게시물 수정
UPDATE board
SET title = '수정 제목'
  , content = '수정 내용'
WHERE board_number = 2;

DELETE FROM image
WHERE board_number = 2;

INSERT INTO image
VALUES (2, 'url');

INSERT INTO image
VALUES (3, 'url3');

-- 게시물 삭제
DELETE FROM comment WHERE board_number = 2;
DELETE FROM favorite WHERE board_number = 2;
DELETE FROM image WHERE board_number = 2;
DELETE FROM board WHERE board_number = 2;

-- 상세 게시물 불러오기
SELECT
    B.board_number AS board_number
  , B.title AS title
  , B.content AS content
  , B.write_datetime AS write_datetime
  , B.writer_email AS writer_email
  , U.nickname AS nickname
  , U.profile_image AS profile_image
FROM board AS B
INNER JOIN user AS U
ON B.writer_email = U.email
WHERE board_number = 2;

SELECT image
FROM image
WHERE board_number = 2;

SELECT
    U.email AS email
  , U.nickname AS nickname
  , U.profile_image AS profile_image
FROM favorite AS F
INNER JOIN user AS U
ON F.user_email = U.email
WHERE F.board_number = 2;

SELECT
    U.nickname AS nickname
  , U.profile_image AS profile_image
  , C.write_datetime AS write_datetime
  , C.content AS content
FROM comment AS C
INNER JOIN user AS U
ON C.user_email = U.email
WHERE C.board_number = 2
ORDER BY write_datetime DESC;

-- 최신 게시물 리스트 불러오기

SELECT *
FROM board AS B
INNER JOIN user AS U
ON B.writer_email = U.email
LEFT JOIN image AS I
ON B.board_number = I.board_number;

-- 최신 게시물 리스트 불러오기 (image row 중복 제거)
-- SELECT
--     B.board_number AS board_number
--   , B.title AS title
--   , B.content AS content
--   , I.image AS image
--   , B.favorite_count AS favorite_count
--   , B.comment_count AS comment_count
--   , B.view_count AS view_count
--   , B.write_datetime AS write_datetime
--   , U.nickname AS writer_nickname
--   , U.profile_image AS writer_profile_image
-- FROM board AS B
-- INNER JOIN user AS U
-- ON B.writer_email = U.email
-- LEFT JOIN (SELECT board_number, ANY_VALUE(image) AS image FROM image GROUP BY board_number) AS I
-- ON B.board_number = I.board_number
SELECT * FROM board_list_view
ORDER BY write_datetime DESC
-- LIMIT 0, 5; -- (페이지 - 1) * 5
LIMIT 5, 5;

-- 검색어 리스트
-- SELECT
--     B.board_number AS board_number
--   , B.title AS title
--   , B.content AS content
--   , I.image AS image
--   , B.favorite_count AS favorite_count
--   , B.comment_count AS comment_count
--   , B.view_count AS view_count
--   , B.write_datetime AS write_datetime
--   , U.nickname AS writer_nickname
--   , U.profile_image AS writer_profile_image
-- FROM board AS B
-- INNER JOIN user AS U
-- ON B.writer_email = U.email
-- LEFT JOIN (SELECT board_number, ANY_VALUE(image) AS image FROM image GROUP BY board_number) AS I
-- ON B.board_number = I.board_number
SELECT * FROM board_list_view
WHERE title LIKE '%수정%' OR content LIKE '%수정%'
ORDER BY write_datetime DESC;

-- 검색어 주간 상위 TOP 3 불러오기
-- SELECT
--     B.board_number AS board_number
--   , B.title AS title
--   , B.content AS content
--   , I.image AS image
--   , B.favorite_count AS favorite_count
--   , B.comment_count AS comment_count
--   , B.view_count AS view_count
--   , B.write_datetime AS write_datetime
--   , U.nickname AS writer_nickname
--   , U.profile_image AS writer_profile_image
-- FROM board AS B
-- INNER JOIN user AS U
-- ON B.writer_email = U.email
-- LEFT JOIN (SELECT board_number, ANY_VALUE(image) AS image FROM image GROUP BY board_number) AS I
-- ON B.board_number = I.board_number
SELECT * FROM board_list_view
WHERE write_datetime BETWEEN '2024-09-02 00:00:00' AND '2024-09-08 23:59:59'
ORDER BY favorite_count DESC, comment_count DESC, view_count DESC, write_datetime DESC
LIMIT 3; -- 3개만

-- 특정 유저 게시물 리스트 불러오기
-- SELECT
--     B.board_number AS board_number
--   , B.title AS title
--   , B.content AS content
--   , I.image AS image
--   , B.favorite_count AS favorite_count
--   , B.comment_count AS comment_count
--   , B.view_count AS view_count
--   , B.write_datetime AS write_datetime
--   , U.nickname AS writer_nickname
--   , U.profile_image AS writer_profile_image
-- FROM board AS B
-- INNER JOIN user AS U
-- ON B.writer_email = U.email
-- LEFT JOIN (SELECT board_number, ANY_VALUE(image) AS image FROM image GROUP BY board_number) AS I
-- ON B.board_number = I.board_number
SELECT * FROM board_list_view
WHERE writer_email = 'email@email.com'
ORDER BY write_datetime DESC, board_number DESC;

-- 인기 검색어 리스트
SELECT search_word, count(search_word) AS count -- search_word를 기준으로 count를 친다.
FROM search_log
WHERE relation IS FALSE -- 관계형이 아닌 것
GROUP BY search_word;   -- search_word로 group by 해준다.
ORDER BY count DESC
LIMIT 15;

-- 관련 검색어 리스트
SELECT relation_word, count(relation_word) AS count
FROM search_log
WHERE search_word = '검색어'
GROUP BY relation_word
ORDER BY count DESC
LIMIT 15;

-- 유저 정보 불러오기 / 로그인 유저 정보 불러오기
SELECT *
FROM user
WHERE email = 'email@email.com';

-- 닉네임 수정하기
UPDATE user
SET nickname = '수정 닉네임'
WHERE email = 'email@email.com';

-- 프로필 이미지 수정
UPDATE user
SET profile_image = 'new_profile_url'
WHERE email = 'email@email.com';

 


출처 : 
https://www.youtube.com/@jiraynorprogramming1589

1. VSCode에서 ERD Editor 설치

 

 

2. API를 기반으로 ERD 작성

 

 

3. DDL 추출

-- Active: 1725368211487@@localhost@3306@board
CREATE TABLE board
(
  board_number   INT         NOT NULL AUTO_INCREMENT COMMENT '게시물 번호',
  title          TEXT        NOT NULL COMMENT '게시물 제목',
  content        TEXT        NOT NULL COMMENT '게시물 내용',
  write_datetime DATETIME    NOT NULL COMMENT '게시물 작성 날짜 및 시간',
  favorite_count INT         NOT NULL DEFAULT 0 COMMENT '게시물 좋아요 수',
  comment_count  INT         NOT NULL DEFAULT 0 COMMENT '게시물 댓글 수',
  view_count     INT         NOT NULL DEFAULT 0 COMMENT '게시물 조회 수',
  writer_email   VARCHAR(50) NOT NULL COMMENT '게시물 작성자 이메일',
  PRIMARY KEY (board_number)
) COMMENT '게시물 테이블';

CREATE TABLE comment
(
  comment_number INT         NOT NULL COMMENT '댓글 번호',
  content        TEXT        NOT NULL COMMENT '댓글 내용',
  write_datetime DATETIME    NOT NULL COMMENT '작성 날짜 및 시간',
  user_email     VARCHAR(50) NOT NULL COMMENT '사용자 이메일',
  board_number   INT         NOT NULL COMMENT '게시물 번호',
  PRIMARY KEY (comment_number)
) COMMENT '댓글 테이블';

CREATE TABLE favorite
(
  user_email   VARCHAR(50) NOT NULL COMMENT '사용자 이메일',
  board_number INT         NOT NULL COMMENT '게시물 번호',
  PRIMARY KEY (user_email, board_number)
) COMMENT '좋아요 테이블';

CREATE TABLE image
(
  board_number INT  NOT NULL COMMENT '게시물 번호',
  image        TEXT NOT NULL COMMENT '게시물 이미지 URL'
) COMMENT '게시물 이미지 테이블';

CREATE TABLE search_log
(
  sequence      INT     NOT NULL AUTO_INCREMENT COMMENT '시퀀스',
  search_word   TEXT    NOT NULL COMMENT '검색어',
  relation_word TEXT    NULL     COMMENT '관련 검색어',
  relation      BOOLEAN NOT NULL COMMENT '관련 검색어 여부',
  PRIMARY KEY (sequence)
) COMMENT '검색 기록 테이블';

CREATE TABLE user
(
  email          VARCHAR(50)  NOT NULL COMMENT '사용자 이메일',
  password       VARCHAR(100) NOT NULL COMMENT '사용자 비밀번호',
  nickname       VARCHAR(20)  NOT NULL UNIQUE COMMENT '사용자 닉네임',
  tel_number     VARCHAR(15)  NOT NULL UNIQUE COMMENT '사용자 휴대전화번호',
  address        TEXT         NOT NULL COMMENT '사용자 주소',
  address_detail TEXT         NULL     COMMENT '사용자 상세 주소',
  profile_image  TEXT         NULL     COMMENT '사용자 프로필 사진 URL',
  PRIMARY KEY (email)
) COMMENT '사용자 테이블';

ALTER TABLE image
  ADD CONSTRAINT FK_board_TO_image
    FOREIGN KEY (board_number)
    REFERENCES board (board_number);

ALTER TABLE board
  ADD CONSTRAINT FK_user_TO_board
    FOREIGN KEY (writer_email)
    REFERENCES user (email);

ALTER TABLE favorite
  ADD CONSTRAINT FK_user_TO_favorite
    FOREIGN KEY (user_email)
    REFERENCES user (email);

ALTER TABLE favorite
  ADD CONSTRAINT FK_board_TO_favorite
    FOREIGN KEY (board_number)
    REFERENCES board (board_number);

ALTER TABLE comment
  ADD CONSTRAINT FK_user_TO_comment
    FOREIGN KEY (user_email)
    REFERENCES user (email);

ALTER TABLE comment
  ADD CONSTRAINT FK_board_TO_comment
    FOREIGN KEY (board_number)
    REFERENCES board (board_number);

 

 

4. developer 사용자 생성 및 권한 부여

CREATE USER 'developer'@'*' IDENTIFIED BY 'P!ssw0rd';

# grant to 'developer'@'*'
GRANT SELECT, UPDATE, DELETE, INSERT
ON board.*
TO 'developer'@'*';

 

출처 : https://www.youtube.com/@jiraynorprogramming1589

1. 프로젝트 주제 : 블로그형 게시판 만들기

 

   [화면 미리보기]

 

2. 기술 스택

 

[백엔드]

 - Java 17

 - Gradle

 

[프론트]

 - React.js

 

[DB]

 - MySQL

 

[IDE]
 - Visual Studio Code

 

3. API 설계

========================================

signIn (로그인)

- request
{
    이메일주소 : 문자열,
    비밀번호 : 문자열
}

- response

성공
{
    코드: 문자열,
    메시지: 문자열
}

실패

- 로그인 실패
Http Status - 401 (인증 실패 Unauthorized)
{
    code: "SF",
    message: "Sign In Failed."
}

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "Database Error."
}

========================================

signUp (회원가입)

- request

{
    "email: String,
    "password: String,
    "nickname: String,
    "telNumber: String, (0을 처음에 받으면 정수로 못받음, 그래서 문자열로 받음)
    "address: String,
    "addressDetail: String
}

- response

성공
Http Status - 200
{
    code: "SU",
    message: "Success.",
    token: "jwt ... token..",
    expiredDate: 123456789
}

실패

- 필수 정보 미입력
Http Status - 400 (Bad Request)
{
    code: "SU",
    message: "Existed Email",
}

- 이메일 중복

Http Status - 400 (Bad Request)
{
    code: "SU",
    message: "Existed Email",
}

- 필수 정보 미입력 / 이메일 포맷 불일치 / 비밀번호 8자 미만 / 전화번호 포맷 불일치
Http Status - 400 (Bad Request)
{
    code: "SU",
    message: "Existed Email",
}

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "Existed Email",
}

========================================

weeklyTop3List (주간 상위 3 게시물 리스트)

- response
Http Status - 200 (OK)
성공
{
    code: "SU",
    message: "Success.",
    top3List: boardListItem[]
}

BoardListItem
{
    boardNumber: int,
    title: String,
    content: String,
    boardTitleImage: string(url),
    favoriteCount: int,
    commentCount: int,
    viewCount: int,
    writeDatetime: String,
    writerNickname: String,
    writerProfileImage: String(url)
}

실패

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

currentList (최신 게시물 리스트)

- response
Http Status - 200 (OK)
성공
{
    code: "SU",
    message: "Success.",
    top3List: boardListItem[]
}

BoardListItem
{
    boardNumber: int,
    title: String,
    content: String,
    boardTitleImage: string(url),
    favoriteCount: int,
    commentCount: int,
    viewCount: int,
    writeDatetime: String,
    writerNickname: String,
    writerProfileImage: String(url)
}

실패

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

popularWordList (인기 검색어 리스트)

- response
Http Status - 200 (OK)
성공
{
    code: "SU",
    message: "Success.",
    popularWordList: String[]
}

실패
- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}
========================================

searchList (검색 게시물 리스트)

- response

성공
Http Status - 200 (OK)
{
    code: "SU",
    message: "Success.",
    searchList: boardListItem[]
}

BoardListItem
{
    boardNumber: int,
    title: String,
    content: String,
    boardTitleImage: string(url),
    favoriteCount: int,
    commentCount: int,
    viewCount: int,
    writeDatetime: String,
    writerNickname: String,
    writerProfileImage: String(url)
}

실패
- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

relativeWordList (관련 검색어 리스트)

- response
Http Status - 200 (OK)
성공
{
    code: "SU",
    message: "Success.",
    relativeWordList: String[]
}

실패
- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}
========================================

boardDetail (게시물 상세 보기)

- response
성공

Http Status - 200 (OK)

{
    code: "SU",
    message: "Success.",
    boardNumber: int,
    title: String,
    content: String,
    boardImage: string[],
    writeDatetime: String,
    writerEmail: String,
    writerNickname: String,
    writerProfileImage: String
}

실패
- 존재하지 않는 게시물
Http Status - 400 (Bad Request)
{
    code: "NB",
    message: "No Exist Board Number",
}

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

favoriteList (좋아요 리스트)

- response

성공

Http Status - 200 (OK)

{
    code: "SU",
    message: "Success.",
    favoriteList: FavoriteListItem[]
}

FavoriteListItem
{
    email: String,
    nickname: String,
    profileImage: String,
}

실패

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

putFavorite (좋아요 기능)

- request

{
    boardNumber: int
}

- response

성공

Http Status - 200 (OK)

{
    code: "SU",
    message: "Success."
}

실패

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

commentList (댓글 리스트)

- response

성공

Http Status - 200 (OK)

{
    code: "SU",
    message: "Success.",
    commentList: CommentListItem[]
}

CommentListItem
{
    email: String,
    nickname: String,
    writeDatetime: String,
    content: String
}

실패

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

postComment (댓글 쓰기)

- request

{
    content: String
}

- response

성공

Http Status - 200 (OK)

{
    code: "SU",
    message: "Success."
}

실패

- 존재하지 않는 게시물

Http Status - 400 (Bad Request)
{
    code: "NB",
    message: "No Existed Board",
}

- 존재하지 않는 유저

Http Status - 400 (Bad Request)
{
    code: "NU",
    message: "No Existed User",
}

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

boardDelete (게시물 삭제)

- response

성공

Http Status - 200 (OK)

{
    code: "SU",
    message: "Success."
}

실패

- 존재하지 않는 유저

Http Status - 400 (Bad Request)
{
    code: "NU",
    message: "No Existed User",
}

- 권한 없음
Http Status - 403 (Forbidden)
{
    code: "NP",
    message: "No Permission",
}

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

boardWrite (게시물 쓰기)
- request

{
    "title": String,
    "title": String,
    boardImageList: string[]
}

- response

성공

Http Status - 200 (OK)

{
    code: "SU",
    message: "Success."
}

실패

- 존재하지 않는 유저

Http Status - 400 (Bad Request)
{
    code: "NU",
    message: "No Existed User",
}

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

boardUpdate (게시물 수정)

- request

{
    "title": String,
    "title": String,
    boardImageList: string[]
}

- response

성공

Http Status - 200 (OK)

{
    code: "SU",
    message: "Success."
}

실패

- 존재하지 않는 유저

Http Status - 400 (Bad Request)
{
    code: "NU",
    message: "No Existed User",
}

- 권한 없음
Http Status - 403 (Forbidden)
{
    code: "NP",
    message: "No Permission",
}

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

getUser (유저 정보 불러오기)

- response

성공

Http Status - 200 (OK)

{
    code: "SU",
    message: "Success.",
    email: String,
    nickname: String,
    profileImage: String
}

실패

- 존재하지 않는 유저

Http Status - 400 (Bad Request)
{
    code: "NU",
    message: "No Existed User",
}

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

userBoardList (특정 유저 게시물 리스트)

- response

성공
Http Status - 200 (OK)
{
    code: "SU",
    message: "Success.",
    boardList: boardListItem[]
}

BoardListItem
{
    boardNumber: int,
    title: String,
    content: String,
    boardTitleImage: string(url),
    favoriteCount: int,
    commentCount: int,
    viewCount: int,
    writeDatetime: String,
    writerNickname: String,
    writerProfileImage: String(url)
}

실패
- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

patchNickname (닉네임 수정)

- request

{
    nickname: String
}

- response

성공

Http Status - 200 (OK)

{
    code: "SU",
    message: "Success."
}

실패

- 존재하지 않는 유저

Http Status - 400 (Bad Request)
{
    code: "NU",
    message: "No Existed User",
}

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

patchProfileImage (유저 프로필 이미지 수정)

- request

{
    profileImage: String
}

- response

성공

Http Status - 200 (OK)

{
    code: "SU",
    message: "Success."
}

실패

- 존재하지 않는 유저

Http Status - 400 (Bad Request)
{
    code: "NU",
    message: "No Existed User",
}

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

fileUpload (파일 업로드)
- request

{
    boardNumber: int
}

- response

성공

Http Status - 200 (OK)

{
    code: "SU",
    message: "Success."
}

실패

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

getFile (파일 불러오기)
- request

{
    boardNumber: int
}

- response

성공

Http Status - 200 (OK)

{
    code: "SU",
    message: "Success."
}

실패

- 데이터베이스 에러
Http Status - 500 (Internal Server Error)
{
    code: "DE",
    message: "",
}

========================================

 

출처 : https://www.youtube.com/@jiraynorprogramming1589

+ Recent posts