diff --git a/README.md b/README.md index 73739fa..2963c57 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ APP_SWAGGER_SERVER_URL=http://localhost:8080 ```bash curl -X POST http://localhost:8080/api/auth/signup \ -H "Content-Type: application/json" \ - -d '{"id":"test_user","password":"password123!","nickname":"테스터"}' + -d '{"id":"testuser","password":"password123!","nickname":"테스터"}' ``` ### 로그인 @@ -94,7 +94,7 @@ curl -X POST http://localhost:8080/api/auth/signup \ ```bash curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ - -d '{"id":"test_user","password":"password123!"}' + -d '{"id":"testuser","password":"password123!"}' ``` 응답은 공통 래퍼 형태이며, `data.accessToken`을 이후 요청에 사용합니다. @@ -131,7 +131,7 @@ curl -X POST http://localhost:8080/api/return-routes/results/{resultId} \ curl -X POST http://localhost:8080/api/companions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer {accessToken}" \ - -d '{"loginId":"friend_user"}' + -d '{"loginId":"frienduser"}' ``` 자세한 요청/응답 스키마는 Swagger에서 확인할 수 있습니다. diff --git a/src/main/java/com/cotato/cokerthon/domain/auth/controller/AuthController.java b/src/main/java/com/cotato/cokerthon/domain/auth/controller/AuthController.java index e21ecdf..3207718 100644 --- a/src/main/java/com/cotato/cokerthon/domain/auth/controller/AuthController.java +++ b/src/main/java/com/cotato/cokerthon/domain/auth/controller/AuthController.java @@ -31,8 +31,9 @@ public AuthController(AuthService authService) { description = """ 아이디, 비밀번호, 닉네임으로 회원가입합니다. - - 아이디는 영문/숫자/밑줄 4~20자이며 중복될 수 없습니다. - - 비밀번호는 6~30자이며 BCrypt로 암호화되어 저장됩니다. + - 닉네임은 한글/영문/숫자 10자 이하입니다. + - 아이디는 소문자 영문/숫자 4~20자이며 중복될 수 없습니다. + - 비밀번호는 영문/숫자/특수문자를 모두 포함한 8~20자이며 BCrypt로 암호화되어 저장됩니다. - 가입 직후 자동 로그인되지 않으므로, 이어서 로그인 API를 호출해 Access Token을 발급받아야 합니다. """ ) diff --git a/src/main/java/com/cotato/cokerthon/domain/auth/dto/request/LoginRequest.java b/src/main/java/com/cotato/cokerthon/domain/auth/dto/request/LoginRequest.java index c2a9473..5565350 100644 --- a/src/main/java/com/cotato/cokerthon/domain/auth/dto/request/LoginRequest.java +++ b/src/main/java/com/cotato/cokerthon/domain/auth/dto/request/LoginRequest.java @@ -5,7 +5,7 @@ @Schema(description = "로그인 요청") public record LoginRequest( - @Schema(description = "로그인 아이디", example = "sleepair_user") + @Schema(description = "로그인 아이디", example = "sleepair123") @NotBlank(message = "아이디는 필수입니다.") String id, diff --git a/src/main/java/com/cotato/cokerthon/domain/auth/dto/request/SignupRequest.java b/src/main/java/com/cotato/cokerthon/domain/auth/dto/request/SignupRequest.java index 71bdae2..4608d97 100644 --- a/src/main/java/com/cotato/cokerthon/domain/auth/dto/request/SignupRequest.java +++ b/src/main/java/com/cotato/cokerthon/domain/auth/dto/request/SignupRequest.java @@ -7,20 +7,25 @@ @Schema(description = "회원가입 요청") public record SignupRequest( - @Schema(description = "로그인 아이디 (영문, 숫자, 밑줄만 사용, 4~20자, 중복 불가)", example = "sleepair_user") + @Schema(description = "로그인 아이디 (소문자 영문, 숫자만 사용, 4~20자, 중복 불가)", example = "sleepair123") @NotBlank(message = "아이디는 필수입니다.") @Size(min = 4, max = 20, message = "아이디는 4자 이상 20자 이하여야 합니다.") - @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "아이디는 영문, 숫자, 밑줄만 사용할 수 있습니다.") + @Pattern(regexp = "^[a-z0-9]+$", message = "아이디는 소문자 영문과 숫자만 사용할 수 있습니다.") String id, - @Schema(description = "비밀번호 (6~30자, 서버에는 BCrypt로 암호화되어 저장됨)", example = "password123!") + @Schema(description = "비밀번호 (영문, 숫자, 특수문자를 모두 포함한 8~20자, 서버에는 BCrypt로 암호화되어 저장됨)", example = "password123!") @NotBlank(message = "비밀번호는 필수입니다.") - @Size(min = 6, max = 30, message = "비밀번호는 6자 이상 30자 이하여야 합니다.") + @Size(min = 8, max = 20, message = "비밀번호는 8자 이상 20자 이하여야 합니다.") + @Pattern( + regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?`~])[A-Za-z\\d!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?`~]+$", + message = "비밀번호는 영문, 숫자, 특수문자를 모두 포함해야 합니다." + ) String password, - @Schema(description = "닉네임 (동행자 카드 등에 노출, 30자 이하)", example = "채은") + @Schema(description = "닉네임 (한글, 영문, 숫자만 사용, 10자 이하)", example = "채은") @NotBlank(message = "닉네임은 필수입니다.") - @Size(max = 30, message = "닉네임은 30자 이하여야 합니다.") + @Size(max = 10, message = "닉네임은 10자 이하여야 합니다.") + @Pattern(regexp = "^[가-힣a-zA-Z0-9]+$", message = "닉네임은 한글, 영문, 숫자만 사용할 수 있습니다.") String nickname ) { } diff --git a/src/main/java/com/cotato/cokerthon/domain/member/dto/response/MemberResponse.java b/src/main/java/com/cotato/cokerthon/domain/member/dto/response/MemberResponse.java index 21d5b0b..97150e7 100644 --- a/src/main/java/com/cotato/cokerthon/domain/member/dto/response/MemberResponse.java +++ b/src/main/java/com/cotato/cokerthon/domain/member/dto/response/MemberResponse.java @@ -12,7 +12,7 @@ public record MemberResponse( @Schema(description = "회원 고유 ID", example = "1") Long memberId, - @Schema(description = "로그인 아이디", example = "sleepair_user") + @Schema(description = "로그인 아이디", example = "sleepair123") String id, @Schema(description = "닉네임", example = "채은") diff --git a/src/test/java/com/cotato/cokerthon/domain/auth/dto/request/SignupRequestValidationTest.java b/src/test/java/com/cotato/cokerthon/domain/auth/dto/request/SignupRequestValidationTest.java new file mode 100644 index 0000000..022b8f3 --- /dev/null +++ b/src/test/java/com/cotato/cokerthon/domain/auth/dto/request/SignupRequestValidationTest.java @@ -0,0 +1,68 @@ +package com.cotato.cokerthon.domain.auth.dto.request; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import java.util.Set; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class SignupRequestValidationTest { + + private static ValidatorFactory validatorFactory; + private static Validator validator; + + @BeforeAll + static void setUp() { + validatorFactory = Validation.buildDefaultValidatorFactory(); + validator = validatorFactory.getValidator(); + } + + @AfterAll + static void tearDown() { + validatorFactory.close(); + } + + @Test + void 회원가입_요청은_화면_제약을_만족하면_유효하다() { + SignupRequest request = new SignupRequest("sleepair123", "password1!", "채은A1"); + + Set> violations = validator.validate(request); + + assertThat(violations).isEmpty(); + } + + @Test + void 닉네임은_필수이며_한글_영문_숫자_10자_이하여야_한다() { + assertThat(validate(new SignupRequest("sleepair123", "password1!", ""))).isNotEmpty(); + assertThat(validate(new SignupRequest("sleepair123", "password1!", "abcdefghijk"))).isNotEmpty(); + assertThat(validate(new SignupRequest("sleepair123", "password1!", "채은!"))).isNotEmpty(); + } + + @Test + void 아이디는_필수이며_소문자_영문과_숫자_4자_이상_20자_이하여야_한다() { + assertThat(validate(new SignupRequest("", "password1!", "채은"))).isNotEmpty(); + assertThat(validate(new SignupRequest("abc", "password1!", "채은"))).isNotEmpty(); + assertThat(validate(new SignupRequest("abcdefghijklmnopqrstu", "password1!", "채은"))).isNotEmpty(); + assertThat(validate(new SignupRequest("Sleepair123", "password1!", "채은"))).isNotEmpty(); + assertThat(validate(new SignupRequest("sleep_air", "password1!", "채은"))).isNotEmpty(); + } + + @Test + void 비밀번호는_필수이며_영문_숫자_특수문자를_포함한_8자_이상_20자_이하여야_한다() { + assertThat(validate(new SignupRequest("sleepair123", "", "채은"))).isNotEmpty(); + assertThat(validate(new SignupRequest("sleepair123", "pass1!", "채은"))).isNotEmpty(); + assertThat(validate(new SignupRequest("sleepair123", "passwordpassword123!!", "채은"))).isNotEmpty(); + assertThat(validate(new SignupRequest("sleepair123", "password!", "채은"))).isNotEmpty(); + assertThat(validate(new SignupRequest("sleepair123", "password1", "채은"))).isNotEmpty(); + assertThat(validate(new SignupRequest("sleepair123", "12345678!", "채은"))).isNotEmpty(); + } + + private Set> validate(SignupRequest request) { + return validator.validate(request); + } +} diff --git a/src/test/java/com/cotato/cokerthon/domain/companion/CompanionIntegrationTest.java b/src/test/java/com/cotato/cokerthon/domain/companion/CompanionIntegrationTest.java index c9b77af..3b55b36 100644 --- a/src/test/java/com/cotato/cokerthon/domain/companion/CompanionIntegrationTest.java +++ b/src/test/java/com/cotato/cokerthon/domain/companion/CompanionIntegrationTest.java @@ -26,25 +26,25 @@ class CompanionIntegrationTest { @Test void 아이디로_동행자를_검색한다() throws Exception { - signupAndLogin("comp_search_me", "password1!", "나"); - signupAndLogin("comp_search_target", "password1!", "민주"); - String accessToken = login("comp_search_me", "password1!"); + signupAndLogin("compsearchme", "password1!", "나"); + signupAndLogin("compsearchtarget", "password1!", "민주"); + String accessToken = login("compsearchme", "password1!"); - ResponseEntity response = search(accessToken, "comp_search_target"); + ResponseEntity response = search(accessToken, "compsearchtarget"); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); JsonNode data = objectMapper.readTree(response.getBody()).path("data"); - assertThat(data.path("loginId").asText()).isEqualTo("comp_search_target"); + assertThat(data.path("loginId").asText()).isEqualTo("compsearchtarget"); assertThat(data.path("nickname").asText()).isEqualTo("민주"); assertThat(data.path("alreadyCompanion").asBoolean()).isFalse(); } @Test void 동행자를_추가하면_즉시_목록에_나타난다() throws Exception { - String accessToken = signupAndLogin("comp_add_me", "password1!", "나"); - signupAndLogin("comp_add_target", "password1!", "민지"); + String accessToken = signupAndLogin("compaddme", "password1!", "나"); + signupAndLogin("compaddtarget", "password1!", "민지"); - ResponseEntity addResponse = add(accessToken, "comp_add_target"); + ResponseEntity addResponse = add(accessToken, "compaddtarget"); assertThat(addResponse.getStatusCode()).isEqualTo(HttpStatus.OK); JsonNode addedData = objectMapper.readTree(addResponse.getBody()).path("data"); assertThat(addedData.path("nickname").asText()).isEqualTo("민지"); @@ -63,8 +63,8 @@ class CompanionIntegrationTest { @Test void 동행자의_수면시차_계산_기록이_있으면_카드에_함께_내려온다() throws Exception { - String myToken = signupAndLogin("comp_city_me", "password1!", "나"); - String friendToken = signupAndLogin("comp_city_friend", "password1!", "민지"); + String myToken = signupAndLogin("compcityme", "password1!", "나"); + String friendToken = signupAndLogin("compcityfriend", "password1!", "민지"); // 기획안 예시와 동일한 입력 (03:00~10:00 / 23:00~07:00) → 3시간30분, WEST(gap=210: 뉴델리/콜롬보 tie) String jetlagBody = """ @@ -80,7 +80,7 @@ class CompanionIntegrationTest { jetlagHeaders.setBearerAuth(friendToken); restTemplate.postForEntity("/api/sleep/jetlag", new HttpEntity<>(jetlagBody, jetlagHeaders), String.class); - add(myToken, "comp_city_friend"); + add(myToken, "compcityfriend"); ResponseEntity listResponse = getCompanions(myToken); JsonNode companion = objectMapper.readTree(listResponse.getBody()).path("data").get(0); @@ -95,30 +95,30 @@ class CompanionIntegrationTest { @Test void 자기_자신은_동행자로_추가할_수_없다() throws Exception { - String accessToken = signupAndLogin("comp_self", "password1!", "나"); + String accessToken = signupAndLogin("compself", "password1!", "나"); - ResponseEntity response = add(accessToken, "comp_self"); + ResponseEntity response = add(accessToken, "compself"); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); } @Test void 같은_동행자를_중복으로_추가할_수_없다() throws Exception { - String accessToken = signupAndLogin("comp_dup_me", "password1!", "나"); - signupAndLogin("comp_dup_target", "password1!", "민지"); + String accessToken = signupAndLogin("compdupme", "password1!", "나"); + signupAndLogin("compduptarget", "password1!", "민지"); - add(accessToken, "comp_dup_target"); - ResponseEntity secondAdd = add(accessToken, "comp_dup_target"); + add(accessToken, "compduptarget"); + ResponseEntity secondAdd = add(accessToken, "compduptarget"); assertThat(secondAdd.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); } @Test void 동행자를_삭제하면_목록에서_사라진다() throws Exception { - String accessToken = signupAndLogin("comp_del_me", "password1!", "나"); - signupAndLogin("comp_del_target", "password1!", "민지"); + String accessToken = signupAndLogin("compdelme", "password1!", "나"); + signupAndLogin("compdeltarget", "password1!", "민지"); - ResponseEntity addResponse = add(accessToken, "comp_del_target"); + ResponseEntity addResponse = add(accessToken, "compdeltarget"); Long companionMemberId = objectMapper.readTree(addResponse.getBody()) .path("data").path("companionMemberId").asLong(); diff --git a/src/test/java/com/cotato/cokerthon/domain/member/MemberIntegrationTest.java b/src/test/java/com/cotato/cokerthon/domain/member/MemberIntegrationTest.java index 3d8f36a..4a0f4f5 100644 --- a/src/test/java/com/cotato/cokerthon/domain/member/MemberIntegrationTest.java +++ b/src/test/java/com/cotato/cokerthon/domain/member/MemberIntegrationTest.java @@ -26,7 +26,7 @@ class MemberIntegrationTest { @Test void 수면시차_계산_전에는_서울이_기본_위치로_내려온다() throws Exception { - String accessToken = signupAndLogin("member_me_before", "password1!", "채은"); + String accessToken = signupAndLogin("membermebefore", "password1!", "채은"); ResponseEntity response = getMyInfo(accessToken); @@ -43,7 +43,7 @@ class MemberIntegrationTest { @Test void 수면시차_계산_후에는_현재_위치의_위경도가_내려온다() throws Exception { - String accessToken = signupAndLogin("member_me_after", "password1!", "채은"); + String accessToken = signupAndLogin("membermeafter", "password1!", "채은"); // 기획안 예시 (03:00~10:00 / 23:00~07:00) → WEST(gap=210: 뉴델리/콜롬보 tie) String jetlagBody = """ diff --git a/src/test/java/com/cotato/cokerthon/domain/sleep/SleepJetlagIntegrationTest.java b/src/test/java/com/cotato/cokerthon/domain/sleep/SleepJetlagIntegrationTest.java index 1f37bb2..522bb27 100644 --- a/src/test/java/com/cotato/cokerthon/domain/sleep/SleepJetlagIntegrationTest.java +++ b/src/test/java/com/cotato/cokerthon/domain/sleep/SleepJetlagIntegrationTest.java @@ -27,7 +27,7 @@ class SleepJetlagIntegrationTest { @Test void 목표보다_늦게_자는_경우_서쪽_방향_도시로_매칭된다() throws Exception { - String accessToken = signupAndLogin("jetlag_west", "password1!", "웨스트"); + String accessToken = signupAndLogin("jetlagwest", "password1!", "웨스트"); // 기획안 예시: 현재 03:00~10:00, 목표 23:00~07:00 → 시차 3시간30분, WEST(gap=210: 뉴델리/콜롬보 tie) String requestBody = """ @@ -58,7 +58,7 @@ class SleepJetlagIntegrationTest { @Test void 같은_시차_구간에_여러_도시가_있으면_랜덤으로_매칭된다() throws Exception { - String accessToken = signupAndLogin("jetlag_tie", "password1!", "타이"); + String accessToken = signupAndLogin("jetlagtie", "password1!", "타이"); // 현재 01:00~09:00(중간 05:00) vs 목표 00:00~08:00(중간 04:00) → 시차 1시간, WEST // 베이징/싱가포르/타이베이가 같은 시차 구간(gap=60)에 매핑되어 있어 반복 호출 시 셋 다 나와야 한다 @@ -87,7 +87,7 @@ class SleepJetlagIntegrationTest { @Test void 목표보다_일찍_자는_경우_동쪽_방향_도시로_매칭된다() throws Exception { - String accessToken = signupAndLogin("jetlag_east", "password1!", "이스트"); + String accessToken = signupAndLogin("jetlageast", "password1!", "이스트"); // 현재 22:00~06:00(중간 02:00) vs 목표 24:00~08:00(중간 04:00) → 시차 2시간, EAST String requestBody = """ @@ -110,7 +110,7 @@ class SleepJetlagIntegrationTest { @Test void 시차가_거의_없으면_서울로_매칭된다() throws Exception { - String accessToken = signupAndLogin("jetlag_same", "password1!", "세임"); + String accessToken = signupAndLogin("jetlagsame", "password1!", "세임"); String requestBody = """ {