Repository: prgrms-aibe-devcourse/AIBE1-Project2-Team04
Pull request #8: feat: 회원가입 및 로그인 기능 구현
Author: login: bitamin707
Created: 2025-05-02T02:50:00Z | Merged: 2025-05-02T10:13:09Z

**1. 회원가입 구현 (/sign)**

![image](https://github.com/user-attachments/assets/03dc1c14-b945-481a-a0ac-c2f1351bbf32)

![image](https://github.com/user-attachments/assets/0ef6a269-54d1-41af-b0ad-f9f6ed7969f9)

<br></br>

**2. 로그인 구현 (/login)**

![image](https://github.com/user-attachments/assets/d0d705a2-107f-463d-a66f-dd7fe05fdc17)

- jwt refresh token 발급 (cookie 저장)
- refresh token 유효 기간 : 1일

![image](https://github.com/user-attachments/assets/ed819a2d-b14e-4be7-b526-b65d35c7f517)

- jwt access token 발급 (로그인 시 jwt 인증 권한 정보)
- access token 유효 기간 : 10분


**_현재 임시로 메인페이지 만들어서 로그인, 회원가입 페이지 링크 해두었습니다._
_로그인 후 페이지 이동 부분은 Front단에서 응답 받고 JWT Access Token 정보 확인 후 페이지 이동이 구현되야합니다_**

TODO.
- token 만료 시 재발급 코드 구현
- 로그아웃 구현
- 로그인 후 토큰 정보 Front단에서 받는 기능 구현


Diff:
diff --git a/src/main/java/com/reboot/auth/config/SecurityConfig.java b/src/main/java/com/reboot/auth/config/SecurityConfig.java
new file mode 100644
index 0000000..f86ede3
--- /dev/null
+++ b/src/main/java/com/reboot/auth/config/SecurityConfig.java
@@ -0,0 +1,69 @@
+package com.reboot.auth.config;
+
+import com.reboot.auth.jwt.JwtAuthenticationFilter;
+import com.reboot.auth.jwt.JwtTokenProvider;
+import com.reboot.auth.jwt.LoginFilter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+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.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.factory.PasswordEncoderFactories;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+
+    private final AuthenticationConfiguration authenticationConfiguration;
+    private final JwtTokenProvider jwtTokenProvider;
+
+    public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JwtTokenProvider jwtTokenProvider) {
+
+        this.authenticationConfiguration = authenticationConfiguration;
+        this.jwtTokenProvider = jwtTokenProvider;
+    }
+
+    @Bean
+    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
+
+        return configuration.getAuthenticationManager();
+    }
+
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
+    }
+
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http
+                .csrf(AbstractHttpConfigurer::disable)
+                .formLogin(AbstractHttpConfigurer::disable)
+                .httpBasic(AbstractHttpConfigurer::disable);
+
+        // 경로별 인가 작업
+        http
+                .authorizeHttpRequests((auth) -> auth
+                        .requestMatchers("/","/login", "/sign").permitAll()
+                        .requestMatchers("/admin").hasRole("ADMIN")
+                        .anyRequest().permitAll());
+
+        http
+                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), LoginFilter.class);
+
+        http
+                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
+
+        http
+                .sessionManagement((session) ->
+                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
+
+        return http.build();
+    }
+}
diff --git a/src/main/java/com/reboot/auth/controller/AuthController.java b/src/main/java/com/reboot/auth/controller/AuthController.java
index 9635663..90d1a52 100644
--- a/src/main/java/com/reboot/auth/controller/AuthController.java
+++ b/src/main/java/com/reboot/auth/controller/AuthController.java
@@ -1,4 +1,28 @@
 package com.reboot.auth.controller;
 
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+@Controller
 public class AuthController {
+
+    @GetMapping
+    public String index() {
+        return "index";
+    }
+
+    @GetMapping("/login")
+    public String login() {
+        return "auth/login";
+    }
+
+    @GetMapping("/sign")
+    public String sign() {
+        return "auth/signup";
+    }
+
+    @GetMapping("/admin")
+    public String admin() {
+        return "auth/admin";
+    }
 }
diff --git a/src/main/java/com/reboot/auth/controller/SignupController.java b/src/main/java/com/reboot/auth/controller/SignupController.java
new file mode 100644
index 0000000..5062199
--- /dev/null
+++ b/src/main/java/com/reboot/auth/controller/SignupController.java
@@ -0,0 +1,24 @@
+package com.reboot.auth.controller;
+
+import com.reboot.auth.dto.SignupDTO;
+import com.reboot.auth.service.SignupService;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PostMapping;
+
+@Controller
+public class SignupController {
+
+    private final SignupService signupService;
+
+    public SignupController(SignupService signupService) {
+        this.signupService = signupService;
+    }
+
+    @PostMapping("/sign")
+    public String signupProcess(SignupDTO signupDTO) {
+        if (signupService.signupProcess(signupDTO)) {
+            return "auth/login";
+        }
+        return "redirect:/sign";
+    }
+}
diff --git a/src/main/java/com/reboot/auth/dto/AuthResponse.java b/src/main/java/com/reboot/auth/dto/AuthResponse.java
deleted file mode 100644
index 0e26def..0000000
--- a/src/main/java/com/reboot/auth/dto/AuthResponse.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.reboot.auth.dto;
-
-public class AuthResponse {
-}
diff --git a/src/main/java/com/reboot/auth/dto/LoginRequest.java b/src/main/java/com/reboot/auth/dto/LoginRequest.java
deleted file mode 100644
index 60e1da5..0000000
--- a/src/main/java/com/reboot/auth/dto/LoginRequest.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.reboot.auth.dto;
-
-public class LoginRequest {
-}
diff --git a/src/main/java/com/reboot/auth/dto/SignupDTO.java b/src/main/java/com/reboot/auth/dto/SignupDTO.java
new file mode 100644
index 0000000..5e2d4d8
--- /dev/null
+++ b/src/main/java/com/reboot/auth/dto/SignupDTO.java
@@ -0,0 +1,10 @@
+package com.reboot.auth.dto;
+
+public record SignupDTO(
+        String username,
+        String password,
+        String name,
+        String email,
+        String nickname
+) {
+}
diff --git a/src/main/java/com/reboot/auth/dto/SignupRequest.java b/src/main/java/com/reboot/auth/dto/SignupRequest.java
deleted file mode 100644
index d9ef22f..0000000
--- a/src/main/java/com/reboot/auth/dto/SignupRequest.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.reboot.auth.dto;
-
-public class SignupRequest {
-}
diff --git a/src/main/java/com/reboot/auth/entity/Game.java b/src/main/java/com/reboot/auth/entity/Game.java
new file mode 100644
index 0000000..d6229f1
--- /dev/null
+++ b/src/main/java/com/reboot/auth/entity/Game.java
@@ -0,0 +1,27 @@
+package com.reboot.auth.entity;
+
+import jakarta.persistence.*;
+import lombok.Data;
+
+@Entity
+@Table(name = "game")
+@Data
+public class Game {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "game_id")
+    private Long gameId;
+
+    @ManyToOne
+    @JoinColumn(name = "member_id")
+    private Member member;
+
+    @Column(name = "game_type")
+    private String gameType;
+
+    @Column(name = "game_tier")
+    private String gameTier;
+
+    @Column(name = "game_position")
+    private String gamePosition;
+}
diff --git a/src/main/java/com/reboot/auth/entity/Instructor.java b/src/main/java/com/reboot/auth/entity/Instructor.java
index 891e96d..72c4c62 100644
--- a/src/main/java/com/reboot/auth/entity/Instructor.java
+++ b/src/main/java/com/reboot/auth/entity/Instructor.java
@@ -1,4 +1,34 @@
 package com.reboot.auth.entity;
 
+import jakarta.persistence.*;
+import lombok.Data;
+
+@Entity
+@Table(name = "instructor")
+@Data
 public class Instructor {
+
+    @Id
+    @Column(name = "instructor_id")
+    private Long instructorId;
+
+    @OneToOne
+    @JoinColumn(name = "instructor_id")
+    private Member member;
+
+    @Column(name = "member_id", insertable = false, updatable = false)
+    private Long memberId;
+
+    // 강사정보
+    @Column(length = 1000)
+    private String career;
+
+    @Column(length = 2000)
+    private String description;
+
+    @Column(name = "review_num")
+    private int reviewNum = 0;
+
+    @Column(name = "rating")
+    private double rating = 0.0;
 }
diff --git a/src/main/java/com/reboot/auth/entity/Member.java b/src/main/java/com/reboot/auth/entity/Member.java
new file mode 100644
index 0000000..0bce88d
--- /dev/null
+++ b/src/main/java/com/reboot/auth/entity/Member.java
@@ -0,0 +1,45 @@
+package com.reboot.auth.entity;
+
+import jakarta.persistence.*;
+import lombok.Data;
+
+
+@Entity
+@Table(name = "member")
+@Data
+public class Member {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "member_id")
+    private Long memberId;
+
+    //인증관련
+    @Column(nullable = false, unique = true)
+    private String username;
+
+    @Column(nullable = false)
+    private String password;
+
+    //기본정보
+    @Column(nullable = false)
+    private String name;
+
+    @Column(name ="e_mail",nullable = false, unique = true)
+    private String email;
+
+    @Column(unique = true, nullable = false)
+    private String nickname;
+
+    @Column(name = "profile_image")
+    private String profileImage;
+
+    private String phone;
+
+    @Column(name = "role")
+    private String role;
+
+    //맞춤 강사 서비스
+    @Column(name = "matching_service")
+    private Boolean matchingService = false;
+}
diff --git a/src/main/java/com/reboot/auth/entity/Student.java b/src/main/java/com/reboot/auth/entity/Student.java
deleted file mode 100644
index 553a427..0000000
--- a/src/main/java/com/reboot/auth/entity/Student.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.reboot.auth.entity;
-
-public class Student {
-}
diff --git a/src/main/java/com/reboot/auth/entity/User.java b/src/main/java/com/reboot/auth/entity/User.java
deleted file mode 100644
index 6edcfe1..0000000
--- a/src/main/java/com/reboot/auth/entity/User.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.reboot.auth.entity;
-
-public class User {
-}
diff --git a/src/main/java/com/reboot/auth/jwt/JwtAuthenticationFilter.java b/src/main/java/com/reboot/auth/jwt/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..605b5c3
--- /dev/null
+++ b/src/main/java/com/reboot/auth/jwt/JwtAuthenticationFilter.java
@@ -0,0 +1,54 @@
+package com.reboot.auth.jwt;
+
+import io.jsonwebtoken.ExpiredJwtException;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+    private final JwtTokenProvider jwtTokenProvider;
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException {
+        String accessToken = req.getHeader("access");
+
+        if (accessToken == null) {
+            chain.doFilter(req, res);
+            return;
+        }
+
+        // 만료 여부 확인
+        try {
+            jwtTokenProvider.validateToken(accessToken);
+        } catch (ExpiredJwtException e) {
+            PrintWriter writer = res.getWriter();
+            writer.println("Expired Access Token");
+
+            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            return;
+        }
+
+        String category = jwtTokenProvider.getCategory(accessToken);
+        if (!category.equals("access")) {
+            PrintWriter writer = res.getWriter();
+            writer.println("Invalid Access Token");
+
+            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            return;
+        }
+
+        Authentication auth = jwtTokenProvider.getAuthentication(accessToken);
+        SecurityContextHolder.getContext().setAuthentication(auth);
+
+        chain.doFilter(req, res);
+    }
+}
diff --git a/src/main/java/com/reboot/auth/jwt/JwtTokenProvider.java b/src/main/java/com/reboot/auth/jwt/JwtTokenProvider.java
new file mode 100644
index 0000000..f1ffbaf
--- /dev/null
+++ b/src/main/java/com/reboot/auth/jwt/JwtTokenProvider.java
@@ -0,0 +1,134 @@
+package com.reboot.auth.jwt;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import lombok.extern.java.Log;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+
+@Component
+@Log
+public class JwtTokenProvider {
+
+    @Value("${jwt.secret}")
+    private String secretKey;
+    // @Value("${jwt.expiration-ms}")
+    // private long expirationMs;
+
+    public SecretKey getSecretKey() {
+        return Keys.hmacShaKeyFor(secretKey.getBytes());
+    }
+
+    // jwt Token 생성 (Access Token과 Refresh Token 분리를 위해 expiration 개별 설정)
+    public String generateToken(String category, String username, String role) {
+
+        Instant now = Instant.now();
+        Date expiration = new Date(now.toEpochMilli() + SetExpirationMs(category));
+
+        return Jwts.builder()
+                .claim("category", category)
+                .claim("username", username)
+                .claim("role", role)
+                .issuedAt(Date.from(now))
+                .expiration(expiration)
+                .signWith(getSecretKey(), Jwts.SIG.HS256)
+                .compact();
+    }
+
+    public String getCategory(String token) {
+        try {
+            return parseClaims(token).get("category", String.class);
+        } catch (JwtException e) {
+            throw new BadCredentialsException("Invalid or expired JWT token", e);
+        }
+    }
+
+    public String getUsername(String token) {
+        try {
+            return parseClaims(token).get("username", String.class);
+        } catch (JwtException e) {
+            throw new BadCredentialsException("Invalid or expired JWT token", e);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<String> getRoles(String token) {
+        try {
+            Object rolesObj = parseClaims(token).get("role");
+
+            if (rolesObj instanceof String roleStr) {
+                // 단일 문자열이면 리스트로 감싸서 반환
+                return List.of(roleStr);
+            }
+
+            if (rolesObj instanceof List<?> rolesList) {
+                return rolesList.stream()
+                        .filter(String.class::isInstance)
+                        .map(String.class::cast)
+                        .toList();
+            }
+
+            throw new BadCredentialsException("Invalid roles claim in JWT: unexpected type " + rolesObj.getClass());
+
+        } catch (JwtException e) {
+            throw new BadCredentialsException("Failed to extract roles from token", e);
+        }
+    }
+
+    // jwt Token 검증
+    public boolean validateToken(String token) {
+        try {
+            parseClaims(token); // throw 되지 않으면 유효
+            return true;
+        } catch (JwtException | IllegalArgumentException e) {
+            log.warning("JWT validation failed: " + e.getMessage());
+            return false;
+        }
+    }
+
+    public Authentication getAuthentication(String token) {
+        String username = getUsername(token);
+        List<SimpleGrantedAuthority> authorities = getRoles(token).stream()
+                .map(SimpleGrantedAuthority::new) // "ROLE_ADMIN" 그대로 사용
+                .toList();
+
+        UserDetails user = new User(username, "", authorities);
+        return new UsernamePasswordAuthenticationToken(user, null, authorities);
+    }
+
+    
+    // 공통 Claims Parser
+    private Claims parseClaims(String token) {
+        return Jwts.parser()
+                .verifyWith(getSecretKey())
+                .build()
+                .parseSignedClaims(token)
+                .getPayload();
+    }
+
+    private long SetExpirationMs(String category) {
+        long expirationMs = 0L;
+
+        if(category.equals("access")) {
+            expirationMs = 600000; // 10분
+        }
+        else if(category.equals("refresh")) {
+            expirationMs = 86400000; // 1일
+        }
+
+        return expirationMs;
+    }
+}
diff --git a/src/main/java/com/reboot/auth/jwt/LoginFilter.java b/src/main/java/com/reboot/auth/jwt/LoginFilter.java
new file mode 100644
index 0000000..abd918f
--- /dev/null
+++ b/src/main/java/com/reboot/auth/jwt/LoginFilter.java
@@ -0,0 +1,72 @@
+package com.reboot.auth.jwt;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Iterator;
+
+public class LoginFilter extends UsernamePasswordAuthenticationFilter {
+
+    private final AuthenticationManager authenticationManager;
+    private final JwtTokenProvider jwtTokenProvider;
+
+    public LoginFilter(AuthenticationManager authenticationManager, JwtTokenProvider jwtTokenProvider) {
+
+        this.authenticationManager = authenticationManager;
+        this.jwtTokenProvider = jwtTokenProvider;
+    }
+
+    @Override
+    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
+
+        //클라이언트 요청에서 username, password 추출
+        String username = obtainUsername(request);
+        String password = obtainPassword(request);
+
+        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
+
+        //token에 담은 검증을 위한 AuthenticationManager로 전달
+        return authenticationManager.authenticate(authToken);
+    }
+
+    //로그인 성공 시 실행하는 메소드 (여기서 JWT를 발급)
+    @Override
+    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
+        String username = authentication.getName();
+
+        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
+        Iterator iterator = authorities.iterator();
+        GrantedAuthority grantedAuthority = authorities.iterator().next();
+        String role = grantedAuthority.getAuthority();
+
+        String accessToken = jwtTokenProvider.generateToken("access", username, role);
+        String refreshToken = jwtTokenProvider.generateToken("refresh", username, role);
+
+        response.setHeader("access", accessToken);
+        response.addCookie(createCookie("refresh", refreshToken));
+        response.setStatus(HttpServletResponse.SC_OK);
+    }
+
+    //로그인 실패시 실행하는 메소드
+    @Override
+    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
+        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+    }
+
+    private Cookie createCookie(String key, String vaule) {
+        Cookie cookie = new Cookie(key, vaule);
+        cookie.setMaxAge(24 * 60 * 60); // refreshToken과 동일하게
+        cookie.setHttpOnly(true);
+        return cookie;
+    }
+}
diff --git a/src/main/java/com/reboot/auth/repository/InstructorRepository.java b/src/main/java/com/reboot/auth/repository/InstructorRepository.java
new file mode 100644
index 0000000..a4d133d
--- /dev/null
+++ b/src/main/java/com/reboot/auth/repository/InstructorRepository.java
@@ -0,0 +1,16 @@
+package com.reboot.auth.repository;
+
+import com.reboot.auth.entity.Instructor;
+import com.reboot.auth.entity.Member;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface InstructorRepository extends JpaRepository<Instructor, Long> {
+    Optional<Instructor> findByMember(Member member);
+    Optional<Instructor> findByMemberId(Long memberId);
+    boolean existsByMember(Member member);
+    boolean existsByMemberId(Long memberId);
+}
\ No newline at end of file
diff --git a/src/main/java/com/reboot/auth/repository/MemberRepository.java b/src/main/java/com/reboot/auth/repository/MemberRepository.java
new file mode 100644
index 0000000..cd8cb67
--- /dev/null
+++ b/src/main/java/com/reboot/auth/repository/MemberRepository.java
@@ -0,0 +1,17 @@
+package com.reboot.auth.repository;
+
+import com.reboot.auth.entity.Member;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface MemberRepository extends JpaRepository<Member, Long> {
+    Optional<Member> findByEmail(String email);
+    Optional<Member> findByUsername(String username);
+    Optional<Member> findByNickname(String nickname);
+    boolean existsByEmail(String email);
+    boolean existsByUsername(String username);
+    boolean existsByNickname(String nickname);
+}
diff --git a/src/main/java/com/reboot/auth/repository/UserRepository.java b/src/main/java/com/reboot/auth/repository/UserRepository.java
deleted file mode 100644
index 5c46fa8..0000000
--- a/src/main/java/com/reboot/auth/repository/UserRepository.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.reboot.auth.repository;
-
-public class UserRepository {
-}
diff --git a/src/main/java/com/reboot/auth/service/AuthService.java b/src/main/java/com/reboot/auth/service/AuthService.java
deleted file mode 100644
index b4039ab..0000000
--- a/src/main/java/com/reboot/auth/service/AuthService.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.reboot.auth.service;
-
-public class AuthService {
-}
diff --git a/src/main/java/com/reboot/auth/service/CustomUserDetailsService.java b/src/main/java/com/reboot/auth/service/CustomUserDetailsService.java
new file mode 100644
index 0000000..8cc7731
--- /dev/null
+++ b/src/main/java/com/reboot/auth/service/CustomUserDetailsService.java
@@ -0,0 +1,33 @@
+package com.reboot.auth.service;
+
+import com.reboot.auth.entity.Member;
+import com.reboot.auth.repository.MemberRepository;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Service
+public class CustomUserDetailsService implements UserDetailsService {
+
+    private final MemberRepository memberRepository;
+
+    public CustomUserDetailsService(MemberRepository memberRepository) {
+        this.memberRepository = memberRepository;
+    }
+
+    @Override
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+
+        Optional<Member> member = memberRepository.findByUsername(username);
+
+        return member.map(value -> User.builder()
+                .username(value.getUsername())
+                .password(value.getPassword())
+                .roles(value.getRole())
+                .build()).orElse(null);
+    }
+}
diff --git a/src/main/java/com/reboot/auth/service/SignupService.java b/src/main/java/com/reboot/auth/service/SignupService.java
new file mode 100644
index 0000000..5ba8dde
--- /dev/null
+++ b/src/main/java/com/reboot/auth/service/SignupService.java
@@ -0,0 +1,45 @@
+package com.reboot.auth.service;
+
+import com.reboot.auth.dto.SignupDTO;
+import com.reboot.auth.entity.Member;
+import com.reboot.auth.repository.MemberRepository;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+@Service
+public class SignupService {
+
+    private final MemberRepository memberRepository;
+    private final PasswordEncoder passwordEncoder;
+
+    public SignupService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
+        this.memberRepository = memberRepository;
+        this.passwordEncoder = passwordEncoder;
+    }
+
+    public boolean signupProcess(SignupDTO signupDTO) {
+        if (isUsernameExist(signupDTO.username())) {
+            System.out.printf("이미 등록된 사용자 : %s", signupDTO.username());
+            return false;
+        }
+
+        Member member = createMember(signupDTO);
+        memberRepository.save(member);
+        return true;
+    }
+
+    private boolean isUsernameExist(String username) {
+        return memberRepository.existsByUsername(username);
+    }
+
+    private Member createMember(SignupDTO dto) {
+        Member member = new Member();
+        member.setUsername(dto.username());
+        member.setPassword(passwordEncoder.encode(dto.password()));
+        member.setName(dto.name());
+        member.setEmail(dto.email());
+        member.setNickname(dto.nickname());
+        member.setRole("ADMIN");
+        return member;
+    }
+}
diff --git a/src/main/java/com/reboot/config/JwtTokenProvider.java b/src/main/java/com/reboot/config/JwtTokenProvider.java
deleted file mode 100644
index cfc63aa..0000000
--- a/src/main/java/com/reboot/config/JwtTokenProvider.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.reboot.config;
-
-public class JwtTokenProvider {
-}
diff --git a/src/main/java/com/reboot/config/SecurityConfig.java b/src/main/java/com/reboot/config/SecurityConfig.java
deleted file mode 100644
index 1b8a6e6..0000000
--- a/src/main/java/com/reboot/config/SecurityConfig.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.reboot.config;
-
-public class SecurityConfig {
-}
diff --git a/src/main/resources/templates/auth/login.html b/src/main/resources/templates/auth/login.html
index e69de29..c6eeac8 100644
--- a/src/main/resources/templates/auth/login.html
+++ b/src/main/resources/templates/auth/login.html
@@ -0,0 +1,241 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org" lang="ko">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>로그인 - 게임 코칭 매칭 플랫폼</title>
+    <script src="https://cdn.tailwindcss.com/3.4.16"></script>
+    <script>tailwind.config={theme:{extend:{colors:{primary:'#4F46E5',secondary:'#10B981'},borderRadius:{'none':'0px','sm':'4px',DEFAULT:'8px','md':'12px','lg':'16px','xl':'20px','2xl':'24px','3xl':'32px','full':'9999px','button':'8px'}}}}</script>
+    <link rel="preconnect" href="https://fonts.googleapis.com">
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link href="https://fonts.googleapis.com/css2?family=Pacifico&display=swap" rel="stylesheet">
+    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;600;700&display=swap" rel="stylesheet">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.6.0/remixicon.min.css">
+    <style>
+        :where([class^="ri-"])::before { content: "\f3c2"; }
+        body {
+            font-family: 'Noto Sans KR', sans-serif;
+        }
+        .custom-checkbox {
+            position: relative;
+            display: inline-block;
+            width: 18px;
+            height: 18px;
+            border-radius: 4px;
+            border: 2px solid #d1d5db;
+            background-color: white;
+            transition: all 0.2s;
+        }
+        .custom-checkbox.checked {
+            border-color: #4F46E5;
+            background-color: #4F46E5;
+        }
+        .custom-checkbox.checked::after {
+            content: '';
+            position: absolute;
+            left: 5px;
+            top: 2px;
+            width: 6px;
+            height: 10px;
+            border: solid white;
+            border-width: 0 2px 2px 0;
+            transform: rotate(45deg);
+        }
+        .social-login-btn {
+            transition: all 0.2s;
+        }
+        .social-login-btn:hover {
+            transform: translateY(-2px);
+        }
+    </style>
+</head>
+<body class="bg-gray-50 min-h-screen flex flex-col">
+<!-- 네비게이션 바 -->
+<header class="bg-white shadow-sm sticky top-0 z-50">
+    <div class="container mx-auto px-4 py-3 flex items-center justify-between">
+        <div class="flex items-center">
+            <a href="/" class="text-2xl font-['Pacifico'] text-primary mr-10">logo</a>
+            <nav class="hidden md:flex space-x-8">
+                <a href="#" class="text-gray-800 hover:text-primary font-medium">강의</a>
+                <a href="#" class="text-gray-800 hover:text-primary font-medium">코치찾기</a>
+                <a href="#" class="text-gray-800 hover:text-primary font-medium">리플레이</a>
+                <a href="#" class="text-gray-800 hover:text-primary font-medium">커뮤니티</a>
+            </nav>
+        </div>
+
+        <div class="flex items-center space-x-4">
+            <div class="relative hidden md:block">
+                <input type="text" placeholder="게임, 코치, 강의 검색" class="pl-10 pr-4 py-2 border border-gray-300 rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent w-64">
+                <div class="absolute left-3 top-2.5 w-5 h-5 flex items-center justify-center text-gray-400">
+                    <i class="ri-search-line"></i>
+                </div>
+            </div>
+            <a th:href="@{/login}" class="bg-primary text-white border border-primary hover:bg-primary/90 px-4 py-2 !rounded-button text-sm font-medium whitespace-nowrap">로그인</a>
+            <a th:href="@{/sign}" class="bg-white text-primary border border-primary hover:bg-gray-50 px-4 py-2 !rounded-button text-sm font-medium whitespace-nowrap">회원가입</a>
+            <button class="md:hidden w-10 h-10 flex items-center justify-center">
+                <i class="ri-menu-line text-xl"></i>
+            </button>
+        </div>
+    </div>
+</header>
+
+<!-- 메인 콘텐츠 -->
+<div class="flex-grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
+    <div class="w-full max-w-md">
+        <!-- 뒤로가기 버튼 -->
+        <div class="mb-8">
+            <a href="/" class="inline-flex items-center text-gray-600 hover:text-primary">
+                <i class="ri-arrow-left-s-line mr-1"></i>
+                <span>뒤로가기</span>
+            </a>
+        </div>
+
+        <!-- 로고 및 타이틀 -->
+        <div class="text-center mb-10">
+            <a href="/" class="text-3xl font-['Pacifico'] text-primary inline-block mb-2">logo</a>
+            <h2 class="text-2xl font-bold text-gray-900">로그인</h2>
+            <p class="text-gray-600 mt-2">계정에 로그인하고 다양한 서비스를 이용하세요</p>
+        </div>
+
+        <!-- 오류 메시지 표시 -->
+        <div th:if="${param.error}" class="mb-4 p-4 bg-red-100 border border-red-300 text-red-700 rounded">
+            <p>아이디 또는 비밀번호가 올바르지 않습니다.</p>
+        </div>
+
+        <!-- 로그인 폼 -->
+        <div class="bg-white shadow-sm rounded-lg p-8 mb-6">
+            <form th:action="@{/login}" method="post" class="space-y-6">
+                <div>
+                    <label for="username" class="block text-sm font-medium text-gray-700 mb-1">아이디</label>
+                    <div class="relative">
+                        <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
+                            <i class="ri-user-line"></i>
+                        </div>
+                        <input id="username" name="username" type="text" required
+                               class="pl-10 pr-3 py-3 w-full border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
+                               placeholder="아이디를 입력하세요">
+                    </div>
+                </div>
+
+                <div>
+                    <div class="flex items-center justify-between mb-1">
+                        <label for="password" class="block text-sm font-medium text-gray-700">비밀번호</label>
+                        <a href="#" class="text-xs text-primary hover:text-primary/80">비밀번호 찾기</a>
+                    </div>
+                    <div class="relative">
+                        <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
+                            <i class="ri-lock-line"></i>
+                        </div>
+                        <input id="password" name="password" type="password" required
+                               class="pl-10 pr-10 py-3 w-full border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
+                               placeholder="비밀번호를 입력하세요">
+                        <button type="button" id="togglePassword" class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600">
+                            <i class="ri-eye-off-line"></i>
+                        </button>
+                    </div>
+                </div>
+
+                <div class="flex items-center">
+                    <div class="flex items-center">
+                        <div id="rememberMe" class="custom-checkbox mr-2 cursor-pointer"></div>
+                        <label for="remember-me" class="text-sm text-gray-600 cursor-pointer">로그인 상태 유지</label>
+                    </div>
+                    <input type="hidden" id="remember-me" name="remember-me" value="false">
+                </div>
+
+                <div>
+                    <button type="submit" class="w-full flex justify-center py-3 px-4 border border-transparent !rounded-button shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary whitespace-nowrap">
+                        로그인
+                    </button>
+                </div>
+            </form>
+
+            <div class="mt-6">
+                <div class="relative">
+                    <div class="absolute inset-0 flex items-center">
+                        <div class="w-full border-t border-gray-300"></div>
+                    </div>
+                    <div class="relative flex justify-center text-sm">
+                        <span class="px-2 bg-white text-gray-500">또는</span>
+                    </div>
+                </div>
+
+                <div class="mt-6 grid grid-cols-1 gap-3">
+                    <button type="button" class="social-login-btn w-full inline-flex justify-center py-3 px-4 border border-gray-300 rounded-md shadow-sm bg-[#FEE500] text-sm font-medium text-gray-800 hover:bg-[#FEE500]/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#FEE500] whitespace-nowrap">
+                        <div class="w-5 h-5 mr-2 flex items-center justify-center">
+                            <i class="ri-kakao-talk-fill"></i>
+                        </div>
+                        카카오로 로그인
+                    </button>
+
+                    <button type="button" class="social-login-btn w-full inline-flex justify-center py-3 px-4 border border-gray-300 rounded-md shadow-sm bg-[#03C75A] text-sm font-medium text-white hover:bg-[#03C75A]/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#03C75A] whitespace-nowrap">
+                        <div class="w-5 h-5 mr-2 flex items-center justify-center">
+                            <i class="ri-naver-fill"></i>
+                        </div>
+                        네이버로 로그인
+                    </button>
+
+                    <button type="button" class="social-login-btn w-full inline-flex justify-center py-3 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 whitespace-nowrap">
+                        <div class="w-5 h-5 mr-2 flex items-center justify-center">
+                            <i class="ri-google-fill"></i>
+                        </div>
+                        구글로 로그인
+                    </button>
+                </div>
+            </div>
+        </div>
+
+        <!-- 회원가입 안내 -->
+        <div class="text-center">
+            <p class="text-sm text-gray-600">
+                아직 회원이 아니신가요? <a th:href="@{/sign}" class="font-medium text-primary hover:text-primary/80">회원가입</a>
+            </p>
+        </div>
+    </div>
+</div>
+
+<!-- 간소화된 푸터 -->
+<footer class="bg-gray-900 text-white pt-10 pb-6">
+    <div class="container mx-auto px-4 text-center">
+        <p class="text-gray-400">© 2025 게임 코칭 매칭 플랫폼. All rights reserved.</p>
+    </div>
+</footer>
+
+<script>
+    document.addEventListener('DOMContentLoaded', function() {
+        // 비밀번호 표시/숨김 토글
+        const togglePassword = document.getElementById('togglePassword');
+        const password = document.getElementById('password');
+        const rememberMeCheckbox = document.getElementById('rememberMe');
+        const rememberMeInput = document.getElementById('remember-me');
+
+        togglePassword.addEventListener('click', function() {
+            const type = password.getAttribute('type') === 'password' ? 'text' : 'password';
+            password.setAttribute('type', type);
+
+            const icon = this.querySelector('i');
+            if (type === 'password') {
+                icon.classList.remove('ri-eye-line');
+                icon.classList.add('ri-eye-off-line');
+            } else {
+                icon.classList.remove('ri-eye-off-line');
+                icon.classList.add('ri-eye-line');
+            }
+        });
+
+        // 로그인 상태 유지 체크박스
+        rememberMeCheckbox.addEventListener('click', function() {
+            const isChecked = this.classList.toggle('checked');
+            rememberMeInput.value = isChecked;
+        });
+
+        // 로그인 폼 제출
+        const loginForm = document.querySelector('form');
+
+        loginForm.addEventListener('submit', function(e) {
+            // 폼 검증이 필요한 경우 여기에 구현
+        });
+    });
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/main/resources/templates/auth/signup.html b/src/main/resources/templates/auth/signup.html
index e69de29..7a22b74 100644
--- a/src/main/resources/templates/auth/signup.html
+++ b/src/main/resources/templates/auth/signup.html
@@ -0,0 +1,470 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org" lang="ko">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>회원가입 - 게임 코칭 매칭 플랫폼</title>
+    <script src="https://cdn.tailwindcss.com/3.4.16"></script>
+    <script>tailwind.config={theme:{extend:{colors:{primary:'#4F46E5',secondary:'#10B981'},borderRadius:{'none':'0px','sm':'4px',DEFAULT:'8px','md':'12px','lg':'16px','xl':'20px','2xl':'24px','3xl':'32px','full':'9999px','button':'8px'}}}}</script>
+    <link rel="preconnect" href="https://fonts.googleapis.com">
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link href="https://fonts.googleapis.com/css2?family=Pacifico&display=swap" rel="stylesheet">
+    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;600;700&display=swap" rel="stylesheet">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.6.0/remixicon.min.css">
+    <style>
+        :where([class^="ri-"])::before { content: "\f3c2"; }
+        body {
+            font-family: 'Noto Sans KR', sans-serif;
+        }
+        .custom-checkbox {
+            position: relative;
+            display: inline-block;
+            width: 18px;
+            height: 18px;
+            border-radius: 4px;
+            border: 2px solid #d1d5db;
+            background-color: white;
+            transition: all 0.2s;
+        }
+        .custom-checkbox.checked {
+            border-color: #4F46E5;
+            background-color: #4F46E5;
+        }
+        .custom-checkbox.checked::after {
+            content: '';
+            position: absolute;
+            left: 5px;
+            top: 2px;
+            width: 6px;
+            height: 10px;
+            border: solid white;
+            border-width: 0 2px 2px 0;
+            transform: rotate(45deg);
+        }
+        .social-login-btn {
+            transition: all 0.2s;
+        }
+        .social-login-btn:hover {
+            transform: translateY(-2px);
+        }
+        .password-strength {
+            height: 4px;
+            transition: all 0.3s;
+        }
+    </style>
+</head>
+<body class="bg-gray-50 min-h-screen flex flex-col">
+<!-- 네비게이션 바 -->
+<header class="bg-white shadow-sm sticky top-0 z-50">
+    <div class="container mx-auto px-4 py-3 flex items-center justify-between">
+        <div class="flex items-center">
+            <a href="/" class="text-2xl font-['Pacifico'] text-primary mr-10">logo</a>
+            <nav class="hidden md:flex space-x-8">
+                <a href="#" class="text-gray-800 hover:text-primary font-medium">강의</a>
+                <a href="#" class="text-gray-800 hover:text-primary font-medium">코치찾기</a>
+                <a href="#" class="text-gray-800 hover:text-primary font-medium">리플레이</a>
+                <a href="#" class="text-gray-800 hover:text-primary font-medium">커뮤니티</a>
+            </nav>
+        </div>
+        <div class="flex items-center space-x-4">
+            <div class="relative hidden md:block">
+                <input type="text" placeholder="게임, 코치, 강의 검색" class="pl-10 pr-4 py-2 border border-gray-300 rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent w-64">
+                <div class="absolute left-3 top-2.5 w-5 h-5 flex items-center justify-center text-gray-400">
+                    <i class="ri-search-line"></i>
+                </div>
+            </div>
+            <a th:href="@{/login}" class="bg-primary text-white border border-primary hover:bg-primary/90 px-4 py-2 !rounded-button text-sm font-medium whitespace-nowrap">로그인</a>
+            <a th:href="@{/sign}" class="bg-white text-primary border border-primary hover:bg-gray-50 px-4 py-2 !rounded-button text-sm font-medium whitespace-nowrap">회원가입</a>
+            <button class="md:hidden w-10 h-10 flex items-center justify-center">
+                <i class="ri-menu-line text-xl"></i>
+            </button>
+        </div>
+    </div>
+</header>
+
+<!-- 메인 콘텐츠 -->
+<div class="flex-grow flex items-center justify-center py-8 px-4 sm:px-6 lg:px-8">
+    <div class="w-full max-w-md">
+        <!-- 뒤로가기 버튼 -->
+        <div class="mb-6">
+            <a href="/" class="inline-flex items-center text-gray-600 hover:text-primary">
+                <i class="ri-arrow-left-s-line mr-1"></i>
+                <span>뒤로가기</span>
+            </a>
+        </div>
+
+        <!-- 로고 및 타이틀 -->
+        <div class="text-center mb-8">
+            <a href="/" class="text-3xl font-['Pacifico'] text-primary inline-block mb-2">logo</a>
+            <h2 class="text-2xl font-bold text-gray-900">회원가입</h2>
+            <p class="text-gray-600 mt-2">계정을 만들고 다양한 서비스를 이용하세요</p>
+        </div>
+
+        <!-- 오류 메시지 표시 -->
+        <div th:if="${error}" class="mb-4 p-4 bg-red-100 border border-red-300 text-red-700 rounded">
+            <p th:text="${error}"></p>
+        </div>
+
+        <!-- 회원가입 폼 -->
+        <div class="bg-white shadow-sm rounded-lg p-8 mb-6">
+            <form th:action="@{/sign}" method="post" class="space-y-5">
+                <div>
+                    <label for="username" class="block text-sm font-medium text-gray-700 mb-1">아이디</label>
+                    <div class="relative">
+                        <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
+                            <i class="ri-user-line"></i>
+                        </div>
+                        <input id="username" name="username" type="text" required
+                               class="pl-10 pr-3 py-3 w-full border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
+                               placeholder="아이디를 입력하세요">
+                    </div>
+                </div>
+
+                <div>
+                    <label for="password" class="block text-sm font-medium text-gray-700 mb-1">비밀번호</label>
+                    <div class="relative">
+                        <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
+                            <i class="ri-lock-line"></i>
+                        </div>
+                        <input id="password" name="password" type="password" required
+                               class="pl-10 pr-10 py-3 w-full border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
+                               placeholder="비밀번호를 입력하세요">
+                        <button type="button" id="togglePassword" class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600">
+                            <i class="ri-eye-off-line"></i>
+                        </button>
+                    </div>
+                    <div class="mt-1">
+                        <div class="w-full bg-gray-200 rounded-full h-1 mt-2">
+                            <div id="passwordStrength" class="password-strength bg-gray-400 rounded-full h-1" style="width: 0%"></div>
+                        </div>
+                        <p id="passwordHint" class="text-xs text-gray-500 mt-1">8자 이상, 대소문자, 숫자, 특수문자 포함</p>
+                    </div>
+                </div>
+
+                <div>
+                    <label for="passwordConfirm" class="block text-sm font-medium text-gray-700 mb-1">비밀번호 확인</label>
+                    <div class="relative">
+                        <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
+                            <i class="ri-lock-line"></i>
+                        </div>
+                        <input id="passwordConfirm" type="password" required
+                               class="pl-10 pr-10 py-3 w-full border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
+                               placeholder="비밀번호를 다시 입력하세요">
+                        <button type="button" id="togglePasswordConfirm" class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600">
+                            <i class="ri-eye-off-line"></i>
+                        </button>
+                    </div>
+                    <p id="passwordMatch" class="text-xs text-gray-500 mt-1 hidden">비밀번호가 일치합니다</p>
+                </div>
+
+                <div>
+                    <label for="name" class="block text-sm font-medium text-gray-700 mb-1">이름</label>
+                    <div class="relative">
+                        <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
+                            <i class="ri-user-line"></i>
+                        </div>
+                        <input id="name" name="name" type="text" required
+                               class="pl-10 pr-3 py-3 w-full border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
+                               placeholder="이름을 입력하세요">
+                    </div>
+                </div>
+
+                <div>
+                    <label for="email" class="block text-sm font-medium text-gray-700 mb-1">이메일</label>
+                    <div class="relative">
+                        <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
+                            <i class="ri-mail-line"></i>
+                        </div>
+                        <input id="email" name="email" type="email" required
+                               class="pl-10 pr-3 py-3 w-full border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
+                               placeholder="이메일을 입력하세요">
+                    </div>
+                </div>
+
+                <div>
+                    <label for="nickname" class="block text-sm font-medium text-gray-700 mb-1">닉네임</label>
+                    <div class="relative">
+                        <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400">
+                            <i class="ri-user-line"></i>
+                        </div>
+                        <input id="nickname" name="nickname" type="text" required
+                               class="pl-10 pr-3 py-3 w-full border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
+                               placeholder="닉네임을 입력하세요">
+                    </div>
+                </div>
+
+                <!-- 약관 동의 섹션 -->
+                <div class="space-y-3 mt-6 pt-4 border-t border-gray-200">
+                    <div class="flex items-center">
+                        <div id="agreeAll" class="custom-checkbox mr-2 cursor-pointer"></div>
+                        <label class="text-sm font-medium text-gray-700 cursor-pointer">모든 약관에 동의합니다</label>
+                    </div>
+
+                    <div class="ml-6 space-y-3">
+                        <div class="flex items-center justify-between">
+                            <div class="flex items-center">
+                                <div id="agreeTerms" class="custom-checkbox mr-2 cursor-pointer"></div>
+                                <label class="text-sm text-gray-600 cursor-pointer">[필수] 서비스 이용약관 동의</label>
+                            </div>
+                            <a href="#" class="text-xs text-primary">보기</a>
+                        </div>
+
+                        <div class="flex items-center justify-between">
+                            <div class="flex items-center">
+                                <div id="agreePrivacy" class="custom-checkbox mr-2 cursor-pointer"></div>
+                                <label class="text-sm text-gray-600 cursor-pointer">[필수] 개인정보 처리방침 동의</label>
+                            </div>
+                            <a href="#" class="text-xs text-primary">보기</a>
+                        </div>
+
+                        <div class="flex items-center justify-between">
+                            <div class="flex items-center">
+                                <div id="agreeMarketing" class="custom-checkbox mr-2 cursor-pointer"></div>
+                                <label class="text-sm text-gray-600 cursor-pointer">[선택] 마케팅 정보 수신 동의</label>
+                            </div>
+                            <a href="#" class="text-xs text-primary">보기</a>
+                        </div>
+                    </div>
+                </div>
+
+                <div>
+                    <button type="submit" id="registerButton"
+                            class="w-full flex justify-center py-3 px-4 border border-transparent !rounded-button shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary whitespace-nowrap">
+                        회원가입
+                    </button>
+                </div>
+            </form>
+
+            <div class="mt-6">
+                <div class="relative">
+                    <div class="absolute inset-0 flex items-center">
+                        <div class="w-full border-t border-gray-300"></div>
+                    </div>
+                    <div class="relative flex justify-center text-sm">
+                        <span class="px-2 bg-white text-gray-500">또는</span>
+                    </div>
+                </div>
+
+                <div class="mt-6 grid grid-cols-1 gap-3">
+                    <button type="button" class="social-login-btn w-full inline-flex justify-center py-3 px-4 border border-gray-300 rounded-md shadow-sm bg-[#FEE500] text-sm font-medium text-gray-800 hover:bg-[#FEE500]/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#FEE500] whitespace-nowrap">
+                        <div class="w-5 h-5 mr-2 flex items-center justify-center">
+                            <i class="ri-kakao-talk-fill"></i>
+                        </div>
+                        카카오로 회원가입
+                    </button>
+                    <button type="button" class="social-login-btn w-full inline-flex justify-center py-3 px-4 border border-gray-300 rounded-md shadow-sm bg-[#03C75A] text-sm font-medium text-white hover:bg-[#03C75A]/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#03C75A] whitespace-nowrap">
+                        <div class="w-5 h-5 mr-2 flex items-center justify-center">
+                            <i class="ri-naver-fill"></i>
+                        </div>
+                        네이버로 회원가입
+                    </button>
+                    <button type="button" class="social-login-btn w-full inline-flex justify-center py-3 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 whitespace-nowrap">
+                        <div class="w-5 h-5 mr-2 flex items-center justify-center">
+                            <i class="ri-google-fill"></i>
+                        </div>
+                        구글로 회원가입
+                    </button>
+                </div>
+            </div>
+        </div>
+
+        <!-- 로그인 안내 -->
+        <div class="text-center">
+            <p class="text-sm text-gray-600">
+                이미 계정이 있으신가요? <a th:href="@{/login}" class="font-medium text-primary hover:text-primary/80">로그인</a>
+            </p>
+        </div>
+    </div>
+</div>
+
+<!-- 간소화된 푸터 -->
+<footer class="bg-gray-900 text-white pt-10 pb-6">
+    <div class="container mx-auto px-4 text-center">
+        <p class="text-gray-400">© 2025 게임 코칭 매칭 플랫폼. All rights reserved.</p>
+    </div>
+</footer>
+
+<script>
+    document.addEventListener('DOMContentLoaded', function() {
+        // 비밀번호 표시/숨김 토글
+        const togglePassword = document.getElementById('togglePassword');
+        const password = document.getElementById('password');
+        togglePassword.addEventListener('click', function() {
+            const type = password.getAttribute('type') === 'password' ? 'text' : 'password';
+            password.setAttribute('type', type);
+            const icon = this.querySelector('i');
+            if (type === 'password') {
+                icon.classList.remove('ri-eye-line');
+                icon.classList.add('ri-eye-off-line');
+            } else {
+                icon.classList.remove('ri-eye-off-line');
+                icon.classList.add('ri-eye-line');
+            }
+        });
+
+        // 비밀번호 확인 표시/숨김 토글
+        const togglePasswordConfirm = document.getElementById('togglePasswordConfirm');
+        const passwordConfirm = document.getElementById('passwordConfirm');
+        togglePasswordConfirm.addEventListener('click', function() {
+            const type = passwordConfirm.getAttribute('type') === 'password' ? 'text' : 'password';
+            passwordConfirm.setAttribute('type', type);
+            const icon = this.querySelector('i');
+            if (type === 'password') {
+                icon.classList.remove('ri-eye-line');
+                icon.classList.add('ri-eye-off-line');
+            } else {
+                icon.classList.remove('ri-eye-off-line');
+                icon.classList.add('ri-eye-line');
+            }
+        });
+
+        // 비밀번호 강도 체크
+        const passwordStrength = document.getElementById('passwordStrength');
+        const passwordHint = document.getElementById('passwordHint');
+
+        password.addEventListener('input', function() {
+            const value = password.value;
+            let strength = 0;
+
+            if (value.length >= 8) strength += 25;
+            if (/[A-Z]/.test(value)) strength += 25;
+            if (/[0-9]/.test(value)) strength += 25;
+            if (/[^A-Za-z0-9]/.test(value)) strength += 25;
+
+            passwordStrength.style.width = strength + '%';
+
+            if (strength <= 25) {
+                passwordStrength.classList.remove('bg-yellow-500', 'bg-green-500');
+                passwordStrength.classList.add('bg-red-500');
+                passwordHint.textContent = '비밀번호가 너무 약합니다';
+            } else if (strength <= 50) {
+                passwordStrength.classList.remove('bg-red-500', 'bg-green-500');
+                passwordStrength.classList.add('bg-yellow-500');
+                passwordHint.textContent = '비밀번호가 적당합니다';
+            } else {
+                passwordStrength.classList.remove('bg-red-500', 'bg-yellow-500');
+                passwordStrength.classList.add('bg-green-500');
+                passwordHint.textContent = '강력한 비밀번호입니다';
+            }
+
+            checkPasswordMatch();
+        });
+
+        // 비밀번호 일치 확인
+        const passwordMatch = document.getElementById('passwordMatch');
+
+        passwordConfirm.addEventListener('input', checkPasswordMatch);
+
+        function checkPasswordMatch() {
+            if (passwordConfirm.value === '') {
+                passwordMatch.classList.add('hidden');
+                return;
+            }
+
+            if (password.value === passwordConfirm.value) {
+                passwordMatch.textContent = '비밀번호가 일치합니다';
+                passwordMatch.classList.remove('text-red-500', 'hidden');
+                passwordMatch.classList.add('text-green-500');
+            } else {
+                passwordMatch.textContent = '비밀번호가 일치하지 않습니다';
+                passwordMatch.classList.remove('text-green-500', 'hidden');
+                passwordMatch.classList.add('text-red-500');
+            }
+        }
+
+        // 약관 동의 체크박스
+        const agreeAll = document.getElementById('agreeAll');
+        const agreeTerms = document.getElementById('agreeTerms');
+        const agreePrivacy = document.getElementById('agreePrivacy');
+        const agreeMarketing = document.getElementById('agreeMarketing');
+        const registerButton = document.getElementById('registerButton');
+
+        // 전체 동의 체크박스 클릭
+        agreeAll.addEventListener('click', function() {
+            const isChecked = this.classList.toggle('checked');
+            if (isChecked) {
+                agreeTerms.classList.add('checked');
+                agreePrivacy.classList.add('checked');
+                agreeMarketing.classList.add('checked');
+            } else {
+                agreeTerms.classList.remove('checked');
+                agreePrivacy.classList.remove('checked');
+                agreeMarketing.classList.remove('checked');
+            }
+            validateForm();
+        });
+
+        // 개별 체크박스 클릭
+        agreeTerms.addEventListener('click', function() {
+            this.classList.toggle('checked');
+            updateAgreeAll();
+            validateForm();
+        });
+
+        agreePrivacy.addEventListener('click', function() {
+            this.classList.toggle('checked');
+            updateAgreeAll();
+            validateForm();
+        });
+
+        agreeMarketing.addEventListener('click', function() {
+            this.classList.toggle('checked');
+            updateAgreeAll();
+        });
+
+        function updateAgreeAll() {
+            if (agreeTerms.classList.contains('checked') &&
+                agreePrivacy.classList.contains('checked') &&
+                agreeMarketing.classList.contains('checked')) {
+                agreeAll.classList.add('checked');
+            } else {
+                agreeAll.classList.remove('checked');
+            }
+        }
+
+        // 폼 유효성 검사
+        function validateForm() {
+            const isTermsChecked = agreeTerms.classList.contains('checked');
+            const isPrivacyChecked = agreePrivacy.classList.contains('checked');
+
+            if (isTermsChecked && isPrivacyChecked) {
+                registerButton.disabled = false;
+                registerButton.classList.remove('opacity-50', 'cursor-not-allowed');
+            } else {
+                registerButton.disabled = true;
+                registerButton.classList.add('opacity-50', 'cursor-not-allowed');
+            }
+        }
+
+        // 초기 폼 유효성 검사
+        validateForm();
+
+        // 회원가입 폼 제출
+        const registerForm = document.querySelector('form');
+        registerForm.addEventListener('submit', function(e) {
+            // 필수 약관 동의 확인
+            if (!agreeTerms.classList.contains('checked') || !agreePrivacy.classList.contains('checked')) {
+                e.preventDefault();
+                alert('필수 약관에 동의해주세요.');
+                return;
+            }
+
+            // 비밀번호 일치 확인
+            if (password.value !== passwordConfirm.value) {
+                e.preventDefault();
+                alert('비밀번호가 일치하지 않습니다.');
+                return;
+            }
+
+            // 마케팅 동의 여부를 hidden input으로 추가
+            const marketingAgreed = agreeMarketing.classList.contains('checked');
+            const marketingInput = document.createElement('input');
+            marketingInput.type = 'hidden';
+            marketingInput.name = 'marketingAgreed';
+            marketingInput.value = marketingAgreed;
+            this.appendChild(marketingInput);
+        });
+    });
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html
new file mode 100644
index 0000000..4708f2f
--- /dev/null
+++ b/src/main/resources/templates/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+</head>
+<body>
+메인 페이지
+<a href="/login">로그인 페이지</a>
+<a href="/sign">회원가입 페이지</a>
+</body>
+</html>
\ No newline at end of file


Reviews:
user: login: LimPark996
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-02T08:01:38Z
user: login: LimPark996
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-02T08:04:45Z
user: login: bitamin707
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-02T08:32:39Z

Review comments:
diff_hunk: @@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+</head>
+<body>
+메인 페이지
+<a href="/login">로그인 페이지</a>
+<a href="/sign">회원가입 페이지</a>
+</body>
+</html>
path: src/main/resources/templates/index.html
user: login: LimPark996
body: 혹시 Aiven은 원규님/정우님 계정으로 진행하고 계시는지, 공용 계정으로 진행하고 계시는지 여쭤봅니다!
created_at: 2025-05-02T08:01:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+</head>
+<body>
+메인 페이지
+<a href="/login">로그인 페이지</a>
+<a href="/sign">회원가입 페이지</a>
+</body>
+</html>
path: src/main/resources/templates/index.html
user: login: bitamin707
body: 아직 개인 계정으로 하고 있습니다
created_at: 2025-05-02T08:32:39Z
author_association: COLLABORATOR

PR comments:
user: login: LimPark996
created_at: 2025-05-02T07:54:20Z
body: ```mermaid
flowchart TD
    %% 사용자 부분
    subgraph USER["사용자(User)"]
        A1["사용자가 회원가입 페이지 접속"] -->|"/sign" URL 접근| A2["회원가입 양식 작성"]
        A2 -->|제출 버튼 클릭| A3["POST 요청 전송"]
    end
    
    %% 시스템 부분
    subgraph SYSTEM["시스템(System)"]
        B1["AuthController - GET '/sign'"] -->|HTML 렌더링| A1
        A3 -->|form 데이터| B2["SignupController - POST '/sign'"]
        B2 -->|데이터 변환| B3["SignupDTO 생성"]
        B3 --> B4["SignupService.signupProcess 호출"]
        B4 -->|username 중복확인| B5{"이미 존재하는 username?"}
        B5 -->|Yes| B6["회원가입 실패"]
        B5 -->|No| B7["Member 엔티티 생성"]
        B7 -->|비밀번호 암호화| B8["PasswordEncoder로 비밀번호 인코딩"]
        B8 --> B9["Member 객체 저장"]
    end
    
    %% 데이터 부분
    subgraph DATA["데이터(Data)"]
        C1[("MemberRepository")]
        B5 -.->|existsByUsername 호출| C1
        B9 -.->|save 호출| C1
    end
    
    %% 결과 및 리다이렉션
    B6 -->|redirect| D1["회원가입 페이지로 재이동"]
    B9 -->|성공| D2["로그인 페이지로 이동"]
    D1 --> A1
    D2 --> E1["로그인 페이지"]
    
    %% 스타일링
    classDef user fill:#c9e6ff,stroke:#0066cc,stroke-width:2px
    classDef system fill:#ffd6cc,stroke:#ff3300,stroke-width:2px
    classDef data fill:#d9f2d9,stroke:#00cc00,stroke-width:2px
    classDef page fill:#fff2cc,stroke:#ffcc00,stroke-width:2px
    
    class A1,A2,A3 user
    class B1,B2,B3,B4,B5,B6,B7,B8,B9 system
    class C1 data
    class D1,D2,E1 page
```
author_association: COLLABORATOR
user: login: LimPark996
created_at: 2025-05-02T07:55:58Z
body: ```mermaid
flowchart TD
    %% 사용자 부분
    subgraph USER["사용자(User)"]
        A1["사용자가 로그인 페이지 접속"] -->|"/login" URL 접근| A2["로그인 양식 작성"]
        A2 -->|로그인 버튼 클릭| A3["POST 요청 전송"]
    end
    
    %% 시스템 부분
    subgraph SYSTEM["시스템(System)"]
        B1["AuthController - GET '/login'"] -->|HTML 렌더링| A1
        A3 -->|username/password| B2["LoginFilter.attemptAuthentication"]
        B2 -->|인증 토큰 생성| B3["UsernamePasswordAuthenticationToken 생성"]
        B3 -->|인증 처리 요청| B4["AuthenticationManager.authenticate"]
        B4 -->|사용자 정보 조회| B5["CustomUserDetailsService.loadUserByUsername"]
        B5 -->|인증 시도| B6{"인증 성공?"}
        B6 -->|Yes| B7["LoginFilter.successfulAuthentication"]
        B6 -->|No| B8["LoginFilter.unsuccessfulAuthentication"]
        B7 -->|JWT 토큰 생성| B9["JwtTokenProvider.generateToken"]
        B9 -->|토큰 응답 설정| B10["Response Header에 Access Token 추가"]
        B10 -->|Cookie 설정| B11["Refresh Token을 쿠키에 저장"]
    end
    
    %% 데이터 부분
    subgraph DATA["데이터(Data)"]
        C1[("MemberRepository")]
        C2["Spring Security Context"]
        B5 -.->|findByUsername 호출| C1
        B7 -.->|Authentication 객체 생성| C2
    end
    
    %% 결과 처리
    B11 -->|로그인 성공| D1["HTTP 200 OK 응답"]
    B8 -->|로그인 실패| D2["HTTP 401 Unauthorized 응답"]
    
    %% JWT 인증 흐름
    E1["이후 요청"] -->|Access Token 포함| E2["JwtAuthenticationFilter.doFilterInternal"]
    E2 -->|토큰 유효성 검증| E3["JwtTokenProvider.validateToken"]
    E3 -->|Claims 추출| E4["Authentication 객체 생성"]
    E4 -.->|인증 정보 저장| C2
    E4 -->|요청 처리 계속| E5["다음 필터로 진행"]
    
    %% 스타일링
    classDef user fill:#c9e6ff,stroke:#0066cc,stroke-width:2px
    classDef system fill:#ffd6cc,stroke:#ff3300,stroke-width:2px
    classDef data fill:#d9f2d9,stroke:#00cc00,stroke-width:2px
    classDef page fill:#fff2cc,stroke:#ffcc00,stroke-width:2px
    classDef jwt fill:#f2d9e6,stroke:#cc00cc,stroke-width:2px
    
    class A1,A2,A3 user
    class B1,B2,B3,B4,B5,B6,B7,B8,B9,B10,B11 system
    class C1,C2 data
    class D1,D2 page
    class E1,E2,E3,E4,E5 jwt
```
author_association: COLLABORATOR
user: login: LimPark996
created_at: 2025-05-02T07:59:51Z
body: ## 상세 프로세스 설명

### 1. 회원가입 프로세스

**사용자(User) 관점:**

사용자가 회원가입 페이지("/sign")에 접속합니다.
양식에 사용자 정보(아이디, 비밀번호, 이름, 이메일, 닉네임)를 입력합니다.
'회원가입' 버튼을 클릭하여 POST 요청을 전송합니다.
성공 시 로그인 페이지로 리다이렉트되고, 실패 시 회원가입 페이지로 다시 이동합니다.

**시스템(System) 관점:**

AuthController가 GET "/sign" 요청을 처리하여 회원가입 페이지(signup.html)를 렌더링합니다.
사용자가 폼을 제출하면 SignupController의 POST "/sign" 메서드가 호출됩니다.
SignupDTO 객체로 사용자 입력 데이터를 캡슐화합니다.
SignupService.signupProcess()가 호출되어 사용자 정보를 처리합니다:

- 사용자명 중복 여부 확인
- 비밀번호 암호화(PasswordEncoder 사용)
- Member 엔티티 생성 및 데이터베이스 저장

결과에 따라 성공 또는 실패 응답을 반환합니다.

**데이터(Data) 관점:**

MemberRepository는 다음 기능을 제공합니다:

- existsByUsername(): 사용자명 중복 검사
- save(): 새 회원 정보 저장


회원 정보는 Member 엔티티로 저장됩니다:

- 인증 정보: username, password, role
- 개인 정보: name, email, nickname

비밀번호는 Password

---

### 2. 로그인 프로세스

**사용자(User) 관점:**

사용자가 로그인 페이지("/login")에 접속합니다.
아이디와 비밀번호를 입력하고 '로그인' 버튼을 클릭합니다.
성공 시 인증 토큰을 받아 후속 요청에 사용하게 되고, 실패 시 오류 메시지를 확인합니다.

**시스템(System) 관점:**

AuthController가 GET "/login" 요청을 처리하여 로그인 페이지(login.html)를 렌더링합니다.
사용자가 로그인 폼을 제출하면 LoginFilter가 요청을 가로챕니다:

- attemptAuthentication(): 인증 시도
- UsernamePasswordAuthenticationToken 생성
- AuthenticationManager를 통한 인증 처리

CustomUserDetailsService가 데이터베이스에서 사용자 정보를 조회합니다.
인증 결과에 따라:

- 성공: successfulAuthentication() 호출
- 실패: unsuccessfulAuthentication() 호출


인증 성공 시:

- JwtTokenProvider를 통해 JWT 토큰 생성
- Access Token을 응답 헤더에 추가
- Refresh Token을 쿠키에 저장

**데이터(Data) 관점:**

MemberRepository에서 사용자 정보 조회:

findByUsername(): 로그인 시도 사용자 정보 조회


**인증 데이터 흐름:**

DB에서 조회된 사용자 정보로 UserDetails 객체 생성
Authentication 객체에 인증 정보 저장
Spring Security Context에 인증 정보 유지


**JWT 토큰 생성 및 관리:**

Access Token(10분) 및 Refresh Token(1일) 생성
토큰에 사용자 정보(username, role) 및 카테고리 정보 포함

---

### 3. JWT 인증 흐름 (후속 요청 처리)

**시스템(System) 관점:**

사용자가 인증이 필요한 리소스에 접근 시 JwtAuthenticationFilter가 요청을 가로챕니다.
요청 헤더의 "access" 값으로 Access Token을 추출합니다.
JwtTokenProvider를 통해 토큰의 유효성을 검증합니다:

만료 여부
토큰 카테고리("access" 여부)

**유효한 토큰이면:**

토큰에서 사용자 정보 및 권한 추출
인증 객체 생성 및 Security Context에 저장
요청 처리 계속

**유효하지 않은 토큰이면:**

401 Unauthorized 응답 반환

**데이터(Data) 관점:**

JWT 토큰 구조:

- 헤더: 알고리즘 정보(HS256)
- 페이로드: category, username, role, 발급시간, 만료시간
- 서명: 비밀키로 서명된 데이터


인증 상태는 SecurityContextHolder에 저장됩니다.
Access Token은 헤더에, Refresh Token은 쿠키에 저장됩니다.
author_association: COLLABORATOR
user: login: uiseon98
created_at: 2025-05-02T08:34:41Z
body: 확인했습니다.
author_association: COLLABORATOR