Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2ba6338
feat(#5): 멤버 컨트롤러 작성
02-wooki Jul 20, 2025
14be12d
docs(#5): 멤버 컨트롤러 문서 작성
02-wooki Jul 20, 2025
9ab3298
feat(#5): 회원가입 요청 바디 작성
02-wooki Jul 20, 2025
812a6b9
feat(#5): 멤버 모델 작성
02-wooki Jul 20, 2025
c66ab46
feat(#5): 멤버 리포지토리 작성
02-wooki Jul 20, 2025
fb4d17e
feat(#5): 멤버 서비스 작성
02-wooki Jul 20, 2025
6e22596
test(#4): 회원가입 컨트롤러 테스트
02-wooki Jul 26, 2025
e2ca641
refactor(#5): Member 패키지 이름 변경
02-wooki Jul 26, 2025
d77833a
feat(#5): 객체 공통 부모클래스 생성
02-wooki Jul 26, 2025
4a7b2d4
feat(#5): BaseEntity 상속 및 birthdate LocalDate 타입으로 변경
02-wooki Jul 26, 2025
ad492b5
refactor(#5): 패키지명 변경으로 인한 import 변경
02-wooki Jul 26, 2025
1c784c9
feat(#5): birthdate 타입 변경
02-wooki Jul 26, 2025
b3c7df1
feat(#5): 멤버모델 팩토리메서드 인자를 dto 타입으로 변경
02-wooki Jul 26, 2025
8e73132
test(#5): 멤버 fixture 작성
02-wooki Jul 26, 2025
831e3d7
test(#5): 회원가입 요청 dto 수정에 따른 변경
02-wooki Jul 26, 2025
22c2247
refactor(#5): 회원가입 컨트롤러 메서드명 변경
02-wooki Aug 2, 2025
0e74233
chore(#5): 비밀번호 해싱을 위한 의존성 추가
02-wooki Aug 2, 2025
fc16c49
feat(#5): 보안 설정 파일 SecurityConfig 작성
02-wooki Aug 2, 2025
ba3eef8
feat(#5): Member 성공, 예외 코드 작성
02-wooki Aug 2, 2025
c4c7dbb
feat(#5): 회원가입 시 생년월일, 전화번호 필드 선택 사항으로 변경 (Nullable)
02-wooki Aug 2, 2025
081d6e5
feat(#5): 회원가입 컨트롤러 리턴 타입 CsResponse 적용
02-wooki Aug 2, 2025
a188e58
feat(#5): 중복 이메일/닉네임 예외처리 및 멤버 팩토리메서드 변경
02-wooki Aug 2, 2025
0af478a
chore(#5): 스웨거 오류응답 예시 변경
02-wooki Aug 2, 2025
5fed50c
docs(#5): 오류응답 예시 업데이트
02-wooki Aug 2, 2025
c150b3a
feat(#5): 회원가입 시 생년월일/전화번호 필드 삭제
02-wooki Aug 2, 2025
414ea2e
feat(#5): 필요 없는 주석 삭제
02-wooki Aug 2, 2025
98bcf63
chore(#5): 유효성 검증 의존성 추가
02-wooki Aug 2, 2025
26c5bca
feat(#5): 이메일 필드 형식 유효성 검증 추가
02-wooki Aug 2, 2025
364454d
feat(#5): 유효성 검사 예외 응답 작성
02-wooki Aug 2, 2025
332a2db
feat(#5): 에러메시지 수정
02-wooki Aug 2, 2025
381b81f
test(#5): 회원가입 컨트롤러 테스트 작성
02-wooki Aug 2, 2025
ebd257a
chore(#5): h2-console 보안 제외
02-wooki Aug 2, 2025
759fe08
test(#5): 회원가입 서비스 테스트
02-wooki Aug 2, 2025
5990838
chore(#5): jacoco 코드 커버리지 사용
02-wooki Aug 2, 2025
55d961f
chore(#5): jacoco 설정 수정
02-wooki Aug 2, 2025
4d31f5b
chore(#5): PR 작성시 테스트 커버리지 측정하도록 github workflow 작성
02-wooki Aug 2, 2025
33846c0
test(#5): 회원가입 서비스 테스트 커버리지
02-wooki Aug 3, 2025
2b2c914
chore(#5): jacoco 코드 커버리지 세팅 수정
02-wooki Aug 3, 2025
2de405c
fix(#5): CI 환경 테스트 프로필 명시
02-wooki Aug 3, 2025
c07c525
fix(#5): CI 환경 테스트 오류 수정
02-wooki Aug 3, 2025
a5781b3
fix(#5): CI환경 테스트 실패
02-wooki Aug 3, 2025
f48fdcd
fix(#5): CI 환경 테스트 오류 수정
02-wooki Aug 3, 2025
3c23675
fix(#5): CI 환경 테스트 오류 수정
02-wooki Aug 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Java CI with Gradle and JaCoCo

on:
pull_request:
branches: [ "main", "develop" ]

permissions:
contents: read
pull-requests: write # PR에 댓글을 쓰기 위한 권한

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3

- name: Run build with Gradle
env:
SPRING_PROFILES_ACTIVE: test
run: ./gradlew build test

- name: Add JaCoCo Report to PR
id: jacoco
uses: madrapps/jacoco-report@v1.6.1
with:
paths: ${{ github.workspace }}/build/reports/jacoco/test/jacocoTestReport.xml
token: ${{ secrets.GITHUB_TOKEN }}
min-coverage-overall: 40 # 전체 커버리지 최소값 (옵션)
min-coverage-changed-files: 60 # 변경된 파일의 커버리지 최소값 (옵션)
title: 'Test Coverage Report'
update-comment: true
39 changes: 39 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.4.5'
id 'io.spring.dependency-management' version '1.1.7'
id 'jacoco'
}

group = 'kr.ac.ks'
Expand All @@ -13,6 +14,10 @@ java {
}
}

jacoco {
toolVersion = "0.8.9"
}

configurations {
compileOnly {
extendsFrom annotationProcessor
Expand All @@ -26,6 +31,8 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
Expand All @@ -39,3 +46,35 @@ dependencies {
tasks.named('test') {
useJUnitPlatform()
}

test {
finalizedBy jacocoTestReport
}

jacocoTestReport {
dependsOn(test)

reports {
xml.required.set(true)
html.required.set(true)

// 생성될 리포트의 위치 지정
xml.destination(file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml"))
html.destination(file("${buildDir}/reports/jacoco/test/jacocoTestReport.html"))
}

afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, excludes: [
"**/*Application.class",
"**/config/**",
"**/common/**",
"**/exeption/**",
"**/response/**",
"**/swagger/**",
"**/code/**",
"**/dto/**"
])
}))
}
}
23 changes: 23 additions & 0 deletions src/main/java/kr/ac/ks/cs_web_back/domain/common/BaseEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kr.ac.ks.cs_web_back.domain.common;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {

@CreatedDate
private LocalDateTime generatedAt;

@LastModifiedDate
private LocalDateTime modifiedAt;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package kr.ac.ks.cs_web_back.domain.member.controller;

import jakarta.validation.Valid;
import kr.ac.ks.cs_web_back.domain.member.controller.code.MemberSuccessCode;
import kr.ac.ks.cs_web_back.domain.member.dto.request.MemberCreateRequest;
import kr.ac.ks.cs_web_back.domain.member.service.MemberService;
import kr.ac.ks.cs_web_back.global.response.CsResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/member")
@RequiredArgsConstructor
public class MemberController implements SpringDocMemberController {

private final MemberService memberService;

@PostMapping("/register")
@ResponseStatus(HttpStatus.CREATED)
public CsResponse<Long> register(
@Valid @RequestBody MemberCreateRequest request
) {
Long id = memberService.createMember(request);
return CsResponse.of(MemberSuccessCode.GENERATED_REGISTERED, id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package kr.ac.ks.cs_web_back.domain.member.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import kr.ac.ks.cs_web_back.domain.member.dto.request.MemberCreateRequest;
import kr.ac.ks.cs_web_back.global.response.CsResponse;
import kr.ac.ks.cs_web_back.global.swagger.error.ApiErrorResponse;
import kr.ac.ks.cs_web_back.global.swagger.error.ErrorCase;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestBody;

@Tag(name = "멤버 API", description = "회원 관리 API")
public interface SpringDocMemberController {

@Operation(summary = "회원가입", description = "회원 가입")
@ApiResponse(responseCode = "201", description = "회원가입에 성공했습니다.")
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/member/register", errorCases = {
@ErrorCase(description = "이메일 없음", code = 9010, exampleMessage = "이메일이 입력되지 않았습니다."),
@ErrorCase(description = "비밀번호 없음", code = 9011, exampleMessage = "비밀번호가 입력되지 않았습니다."),
@ErrorCase(description = "사용자명 없음", code = 9012, exampleMessage = "사용자명이 입력되지 않았습니다.")
})
@ApiErrorResponse(status = HttpStatus.CONFLICT, instance = "/member/register", errorCases = {
@ErrorCase(description = "중복 이메일", code = 5010, exampleMessage = "이미 존재하는 이메일입니다."),
@ErrorCase(description = "중복 사용자명", code = 5011, exampleMessage = "이미 존재하는 사용자명입니다.")
})
CsResponse<Long> register(
@RequestBody MemberCreateRequest request
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package kr.ac.ks.cs_web_back.domain.member.controller.code;

import kr.ac.ks.cs_web_back.global.exeption.dto.ExceptionCode;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum MemberExceptionCode implements ExceptionCode {

BAD_REQUEST_NO_EMAIL(9010, "이메일이 입력되지 않았습니다."),
BAD_REQUEST_NO_PASSWORD(9011, "비밀번호가 입력되지 않았습니다."),
BAD_REQUEST_NO_USERNAME(9012, "사용자명이 입력되지 않았습니다."),
CONFLICT_EMAIL(5010, "이미 존재하는 이메일입니다."),
CONFLICT_USERNAME(5012, "이미 존재하는 사용자명입니다."),
;

private final int code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package kr.ac.ks.cs_web_back.domain.member.controller.code;

import kr.ac.ks.cs_web_back.global.response.SuccessCode;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum MemberSuccessCode implements SuccessCode {

GENERATED_REGISTERED(2101, "회원가입에 성공했습니다."),
OK_WITHDRAWN(2102,"회원 탈퇴에 성공했습니다."),
OK_FOUND_USER_PROFILE(2103, "사용자 정보 조회에 성공했습니다."),
OK_MODIFIED_USER_PROFILE(2104, "사용자 정보 변경에 성공했습니다."),
OK_FOUND_USER_WRITTEN_POSTS(2105, "해당 사용자가 작성한 게시글을 조회했습니다."),
OK_FOUND_USER_WRITTEN_COMMENTS(2106, "해당 사용자가 작성한 댓글을 조회했습니다."),
;

private final int code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kr.ac.ks.cs_web_back.domain.member.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;

@Schema(description = "회원가입")
public record MemberCreateRequest(
@Schema(description = "이메일", example = "example@ks.ac.kr")
@NotBlank(message = "이메일이 입력되지 않았습니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
String email,

@Schema(description = "비밀번호", example = "examplePassword123")
@NotBlank(message = "비밀번호가 입력되지 않았습니다.")
@Size(max = 24, message = "비밀번호는 24자 내로 입력해 주세요.")
String password,

@Schema(description = "사용자명", example = "userNickName")
@NotBlank(message = "사용자명이 입력되지 않았습니다.")
@Size(max = 12, message = "유저명은 12자 내로 입력해 주세요.")
String username
) {
}
43 changes: 43 additions & 0 deletions src/main/java/kr/ac/ks/cs_web_back/domain/member/model/Member.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package kr.ac.ks.cs_web_back.domain.member.model;

import jakarta.persistence.*;
import kr.ac.ks.cs_web_back.domain.common.BaseEntity;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDate;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
private String email;

@Column(nullable = false)
private String password;

@Column(nullable = false)
private String username;

@Column
private LocalDate birthdate;

@Column
private String number;

@Builder
private Member(String email, String password, String username, LocalDate birthdate, String number) {
this.email = email;
this.password = password;
this.username = username;
this.birthdate = birthdate;
this.number = number;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package kr.ac.ks.cs_web_back.domain.member.repository;

import kr.ac.ks.cs_web_back.domain.member.model.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
boolean existsByEmail(String email);
boolean existsByUsername(String username);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package kr.ac.ks.cs_web_back.domain.member.service;

import kr.ac.ks.cs_web_back.domain.member.controller.code.MemberExceptionCode;
import kr.ac.ks.cs_web_back.domain.member.dto.request.MemberCreateRequest;
import kr.ac.ks.cs_web_back.domain.member.model.Member;
import kr.ac.ks.cs_web_back.domain.member.repository.MemberRepository;
import kr.ac.ks.cs_web_back.global.exeption.domain.ConflictException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class MemberService {

private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;

public Long createMember(MemberCreateRequest request) {
if (memberRepository.existsByEmail(request.email()))
throw new ConflictException(MemberExceptionCode.CONFLICT_EMAIL);

if (memberRepository.existsByUsername(request.username()))
throw new ConflictException(MemberExceptionCode.CONFLICT_USERNAME);

String encodedPassword = passwordEncoder.encode(request.password());

Member member = Member.builder()
.email(request.email())
.password(encodedPassword)
.username(request.username())
.build();

return memberRepository.save(member).getId();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
package kr.ac.ks.cs_web_back.global.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
Loading
Loading