Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ perf/env/cloud.env
**/logs/
perf/k6/**/result/*.json
docs
!back/src/main/java/com/back/**/docs/
!back/src/main/java/com/back/**/docs/*.java
AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.back.auth.adapter.in.web.dto.AuthMemberResponse;
import com.back.auth.adapter.in.web.dto.AuthSignupRequest;
import com.back.auth.adapter.in.web.dto.AuthTokenResponse;
import com.back.auth.adapter.in.web.docs.AuthApiDocs;
import com.back.auth.application.AuthErrorCode;
import com.back.auth.application.AuthService;
import com.back.auth.application.AuthSuccessCode;
Expand Down Expand Up @@ -36,7 +37,7 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/auth")
public class AuthController {
public class AuthController implements AuthApiDocs {

private static final String HTTP_SCHEME = "http";
private static final String HTTPS_SCHEME = "https";
Expand Down
173 changes: 173 additions & 0 deletions back/src/main/java/com/back/auth/adapter/in/web/docs/AuthApiDocs.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package com.back.auth.adapter.in.web.docs;

import com.back.auth.adapter.in.web.dto.AuthLoginRequest;
import com.back.auth.adapter.in.web.dto.AuthMemberResponse;
import com.back.auth.adapter.in.web.dto.AuthSignupRequest;
import com.back.auth.adapter.in.web.dto.AuthTokenResponse;
import com.back.global.rsData.RsData;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;

@Tag(
name = "인증",
description =
"이메일 로그인, 토큰 재발급, 현재 세션 확인, OIDC 로그인 진입점을 제공하는 API입니다. "
+ "로그인 성공 시 access token은 응답 본문으로, refresh token은 HttpOnly 쿠키로 발급됩니다.")
public interface AuthApiDocs {

@Operation(
summary = "회원가입",
description =
"이메일, 비밀번호, 닉네임으로 일반 계정을 생성합니다. "
+ "가입 직후 자동 로그인되지는 않으므로, 이후 `/api/v1/auth/login`을 호출해 토큰을 발급받아야 합니다.",
security = {})
@ApiResponses({
@ApiResponse(responseCode = "200", description = "회원가입 성공"),
@ApiResponse(responseCode = "400", description = "입력값 검증 실패 또는 중복 계정")
})
RsData<AuthMemberResponse> signup(
@RequestBody(
required = true,
description = "회원가입 요청 본문",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = AuthSignupRequest.class),
examples =
@ExampleObject(
name = "일반 회원가입",
value =
"""
{
"email": "demo@example.com",
"password": "demo1234!",
"nickname": "마음온데모"
}
""")))
AuthSignupRequest request);

@Operation(
summary = "로그인",
description =
"이메일/비밀번호를 검증한 뒤 access token과 회원 요약 정보를 응답하고, "
+ "refresh token은 HttpOnly 쿠키로 설정합니다. Swagger에서 받은 access token은 우측 상단 Authorize에 넣어 보호 API를 시연할 수 있습니다.",
security = {})
@ApiResponses({
@ApiResponse(responseCode = "200", description = "로그인 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청"),
@ApiResponse(responseCode = "401", description = "이메일 또는 비밀번호 불일치")
})
RsData<AuthTokenResponse> login(
@RequestBody(
required = true,
description = "로그인 요청 본문",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = AuthLoginRequest.class),
examples =
@ExampleObject(
name = "로그인",
value =
"""
{
"email": "demo@example.com",
"password": "demo1234!"
}
""")))
AuthLoginRequest request,
@Parameter(hidden = true) HttpServletResponse response);

@Operation(
summary = "Access 토큰 재발급",
description =
"refresh token 쿠키를 사용해 access token과 refresh token을 함께 재발급합니다. "
+ "클라이언트는 refresh token을 직접 다루지 않고 쿠키만 유지하면 됩니다.",
security = {})
@ApiResponses({
@ApiResponse(responseCode = "200", description = "재발급 성공"),
@ApiResponse(responseCode = "401", description = "refresh token 누락 또는 만료")
})
RsData<AuthTokenResponse> refresh(
@Parameter(hidden = true) HttpServletRequest request,
@Parameter(hidden = true) HttpServletResponse response);

@Operation(
summary = "현재 세션 확인",
description =
"refresh token 쿠키의 유효성만 확인해 현재 세션을 복원합니다. "
+ "토큰 회전 없이 access token payload만 다시 구성하므로 앱 진입 시 세션 복구 용도로 적합합니다.",
security = {})
@ApiResponses({
@ApiResponse(responseCode = "200", description = "세션 확인 성공"),
@ApiResponse(responseCode = "401", description = "유효한 refresh token 없음")
})
RsData<AuthTokenResponse> session(@Parameter(hidden = true) HttpServletRequest request);

@Operation(
summary = "로그아웃",
description =
"refresh token을 폐기하고 관련 쿠키를 만료시킵니다. "
+ "클라이언트는 응답 이후 로컬 access token도 함께 제거해야 안전하게 로그아웃 상태를 유지할 수 있습니다.",
security = {})
@ApiResponses({
@ApiResponse(responseCode = "200", description = "로그아웃 성공")
})
RsData<Void> logout(
@Parameter(hidden = true) HttpServletRequest request,
@Parameter(hidden = true) HttpServletResponse response);

@Operation(
summary = "현재 로그인 사용자 조회",
description =
"Bearer access token 기준으로 현재 인증된 사용자의 프로필 요약 정보를 조회합니다. "
+ "로그인 직후 토큰이 정상 발급되었는지 확인하거나 프론트 초기화 시 현재 사용자 정보를 복원할 때 사용합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "조회 성공"),
@ApiResponse(responseCode = "401", description = "인증 필요")
})
RsData<AuthMemberResponse> me(@Parameter(hidden = true) Authentication authentication);

@Operation(
summary = "OIDC 로그인 시작",
description =
"provider별 인가 URL을 생성하고 해당 주소로 리다이렉트합니다. "
+ "`redirect_uri`는 로그인 완료 후 프론트가 다시 돌아갈 주소이며, 서버 allowlist에 등록된 값만 허용됩니다.",
security = {})
@ApiResponses({
@ApiResponse(responseCode = "302", description = "외부 OIDC 인가 페이지로 리다이렉트"),
@ApiResponse(responseCode = "400", description = "허용되지 않은 redirect_uri 또는 provider")
})
ResponseEntity<Void> startOidcAuthorize(
@Parameter(description = "OIDC provider 식별자. 예: `maum-on-oidc`, `kakao`") String provider,
@Parameter(description = "로그인 완료 후 프론트가 복귀할 주소") String redirectUri,
@Parameter(hidden = true) HttpServletRequest request);

@Operation(
summary = "OIDC 콜백 처리",
description =
"외부 OIDC 제공자가 전달한 code/state를 검증하고 내부 JWT를 발급한 뒤 프론트 리다이렉트 주소로 이동시킵니다. "
+ "일반 사용자는 Swagger에서 직접 호출하기보다 브라우저 로그인 흐름에서 사용하게 됩니다.",
security = {})
@ApiResponses({
@ApiResponse(responseCode = "302", description = "프론트 리다이렉트"),
@ApiResponse(responseCode = "400", description = "code/state 검증 실패")
})
ResponseEntity<Void> handleOidcCallback(
@Parameter(description = "OIDC provider 식별자") String provider,
@Parameter(description = "인가 코드") String code,
@Parameter(description = "인가 요청 시 발급된 state 값") String state,
@Parameter(hidden = true) HttpServletRequest request,
@Parameter(hidden = true) HttpServletResponse response);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
package com.back.auth.adapter.in.web.dto;

import io.swagger.v3.oas.annotations.media.Schema;

/** 로그인 요청 DTO. */
public record AuthLoginRequest(String email, String password) {}
@Schema(description = "이메일 로그인 요청 정보")
public record AuthLoginRequest(
@Schema(description = "가입한 이메일 주소", example = "demo@example.com") String email,
@Schema(description = "로그인 비밀번호", example = "demo1234!") String password) {}
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package com.back.auth.adapter.in.web.dto;

import com.back.member.domain.Member;
import io.swagger.v3.oas.annotations.media.Schema;

/** 인증 API에서 사용하는 회원 정보 응답 DTO. */
@Schema(description = "인증 응답에 포함되는 현재 사용자 요약 정보")
public record AuthMemberResponse(
Long id, String email, String nickname, String role, String status) {
@Schema(description = "회원 식별자", example = "17") Long id,
@Schema(description = "회원 이메일", example = "demo@example.com") String email,
@Schema(description = "표시 닉네임", example = "마음온데모") String nickname,
@Schema(description = "회원 권한", example = "USER") String role,
@Schema(description = "회원 상태", example = "ACTIVE") String status) {

/** Member 엔티티를 인증 응답용 DTO로 변환한다. */
public static AuthMemberResponse from(Member member) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
package com.back.auth.adapter.in.web.dto;

import io.swagger.v3.oas.annotations.media.Schema;

/** 회원 가입 요청 DTO. */
public record AuthSignupRequest(String email, String password, String nickname) {}
@Schema(description = "이메일 기반 일반 회원가입 요청 정보")
public record AuthSignupRequest(
@Schema(description = "중복되지 않는 이메일 주소", example = "demo@example.com") String email,
@Schema(description = "영문, 숫자, 특수문자를 조합한 비밀번호", example = "demo1234!") String password,
@Schema(description = "서비스에서 표시할 닉네임", example = "마음온데모") String nickname) {}
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package com.back.auth.adapter.in.web.dto;

import io.swagger.v3.oas.annotations.media.Schema;

/** 로그인/재발급 성공 시 반환하는 액세스 토큰 응답 DTO. */
@Schema(description = "로그인 또는 재발급 성공 시 반환되는 토큰 응답")
public record AuthTokenResponse(
String accessToken, String tokenType, long expiresInSeconds, AuthMemberResponse member) {}
@Schema(description = "Bearer access token", example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxNyJ9.signature")
String accessToken,
@Schema(description = "토큰 타입", example = "Bearer") String tokenType,
@Schema(description = "액세스 토큰 만료까지 남은 초", example = "3600") long expiresInSeconds,
@Schema(description = "토큰에 매핑된 회원 요약 정보") AuthMemberResponse member) {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.back.censorship.adapter.in.web.dto.AuditAiRequest;
import com.back.censorship.adapter.in.web.dto.AuditAiResponse;
import com.back.censorship.adapter.in.web.docs.AiApiDocs;
import com.back.censorship.application.service.AiService;
import com.back.global.rsData.RsData;
import lombok.RequiredArgsConstructor;
Expand All @@ -13,7 +14,7 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/ai")
public class AiController {
public class AiController implements AiApiDocs {

private final AiService aiService;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.back.censorship.adapter.in.web.docs;

import com.back.censorship.adapter.in.web.dto.AuditAiRequest;
import com.back.censorship.adapter.in.web.dto.AuditAiResponse;
import com.back.global.rsData.RsData;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;

@Tag(
name = "AI 점검",
description =
"작성 콘텐츠의 욕설, 개인정보 노출, 무성의 표현 등을 AI로 점검하는 API입니다. 편지/게시글 작성 전후의 안전장치로 사용할 수 있습니다.")
public interface AiApiDocs {

@Operation(
summary = "콘텐츠 AI 점검",
description =
"입력한 콘텐츠를 AI로 분석해 통과 여부, 위반 유형, 안내 메시지, 요약을 반환합니다. "
+ "`type`에는 `Letter`, `Post` 같은 도메인 힌트를 전달할 수 있습니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "점검 성공"),
@ApiResponse(responseCode = "401", description = "인증 필요")
})
RsData<AuditAiResponse> audit(
@RequestBody(
required = true,
description = "AI 점검 요청",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = AuditAiRequest.class),
examples =
@ExampleObject(
value =
"""
{
"content": "전화번호는 010-1234-5678이고 너무 화가 나.",
"type": "Letter"
}
""")))
AuditAiRequest request);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.back.censorship.adapter.in.web.dto;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "AI 콘텐츠 안전 점검 요청")
public record AuditAiRequest(
@Schema(description = "점검할 원문", example = "전화번호는 010-1234-5678이고 너무 화가 나.")
String content,
String type // Letter, Post
@Schema(description = "콘텐츠 유형", example = "Letter") String type // Letter, Post
) {}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.back.censorship.adapter.in.web.dto;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "AI 콘텐츠 안전 점검 결과")
public record AuditAiResponse(
boolean isPassed,
String violationType, // PROFANITY(욕설), PERSONAL_INFO(개인정보), INSINCERE(무성의), NONE
String message,
String summary
@Schema(description = "통과 여부", example = "false") boolean isPassed,
@Schema(description = "위반 유형", example = "PERSONAL_INFO") String violationType,
@Schema(description = "사용자 안내 메시지", example = "개인정보가 포함되어 있어 수정이 필요합니다.") String message,
@Schema(description = "AI 요약 결과", example = "전화번호가 포함된 표현이 감지되었습니다.") String summary
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


import com.back.auth.application.AuthErrorCode;
import com.back.comment.controller.docs.CommentApiDocs;
import com.back.comment.dto.CommentCreateReq;
import com.back.comment.dto.CommentInfoRes;
import com.back.comment.dto.CommentUpdateReq;
Expand All @@ -22,7 +23,7 @@
@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class CommentController {
public class CommentController implements CommentApiDocs {

private final CommentService commentService;

Expand Down
Loading
Loading