
LANGUAGE
FrameWork
SERVER
DB
MySQL
Post 엔티티, PostRepository 생성PostService 작성PostController에서 CRUD API 구현Long? 관련 Type mismatch 오류 해결User 엔티티 생성 id, username, password, roleUserRepository, UserService 작성AuthController에서 회원가입 API 구현BCrypt 해싱 적용JwtUtil.kt 작성 (토큰 생성, 검증, 사용자 정보 추출)JwtFilter.kt 작성하여 SecurityContext에 인증 정보 저장SecurityConfig.kt에서 Spring Security + JWT 설정JwtAuthentication 관련 오류 해결AdminController에서 관리자 생성 API 구현Use 엔티티에 Role 필드 추가 USER, ADMINAdminController 제거하여 충돌 해결SecurityConfig.kt에서/api/admin/** → ADMIN 권한 필요/api/posts/** → 로그인된 사용자만 접근 가능/api/auth/** → 모든 사용자 접근 가능package backend
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class KotlinProjectApplicationTests {
@Test
fun contextLoads() {
}
}

package backend
import backend.entity.Post
import backend.repository.PostRepository
import backend.service.PostService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.MockitoAnnotations
import java.util.*
class PostServiceTest {
@Mock
private lateinit var postRepository: PostRepository
@InjectMocks
private lateinit var postService: PostService
@BeforeEach
fun setup() {
MockitoAnnotations.openMocks(this)
}
@Test
fun `게시글 생성 테스트`() {
// Given
val title = "테스트 제목"
val content = "테스트 내용"
val author = "테스트 작성자"
val post = Post(id = 1L, title = title, content = content, author = author)
`when`(postRepository.save(any(Post::class.java))).thenReturn(post)
// When
val savedPost = postService.createPost(title, content, author)
// Then
assertNotNull(savedPost)
assertEquals(title, savedPost.title)
assertEquals(content, savedPost.content)
assertEquals(author, savedPost.author)
}
@Test
fun `게시글 단건 조회 테스트`() {
// Given
val post = Post(id = 1L, title = "테스트", content = "테스트 내용", author = "작성자")
`when`(postRepository.findById(1L)).thenReturn(Optional.of(post))
// When
val foundPost = postService.getPostById(1L)
// Then
assertNotNull(foundPost)
assertEquals("테스트", foundPost.title)
}
@Test
fun `게시글 삭제 테스트`() {
// Given
val post = Post(id = 1L, title = "테스트", content = "테스트 내용", author = "작성자")
`when`(postRepository.findById(1L)).thenReturn(Optional.of(post))
doNothing().`when`(postRepository).deleteById(1L) // deleteById()를 모킹
// When
postService.deletePost(1L)
// Then
verify(postRepository, times(1)).deleteById(1L) // deleteById()가 호출되었는지 검증
}
}

package backend.controller
import backend.dto.UserRequest
import backend.dto.UserResponse
import backend.service.UserService
import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.test.web.servlet.setup.MockMvcBuilders
@ExtendWith(MockitoExtension::class) // 최신 Mockito 확장 사용
class UserControllerTest {
private lateinit var mockMvc: MockMvc
@Mock
private lateinit var userService: UserService
@InjectMocks
private lateinit var userController: UserController
private val objectMapper = ObjectMapper()
private lateinit var userRequest: UserRequest
private lateinit var userResponse: UserResponse
@BeforeEach
fun setup() {
mockMvc = MockMvcBuilders.standaloneSetup(userController).build()
// 테스트용 데이터 설정
userRequest = UserRequest(
username = "testuser",
password = "password123"
)
userResponse = UserResponse(
id = 1L,
username = "testuser"
)
}
@Test
fun `회원가입 성공 - 200 OK 응답`() {
`when`(userService.registerUser(any())).thenReturn(userResponse)
mockMvc.perform(
post("/api/users/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(userRequest))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.username").value("testuser"))
verify(userService, times(1)).registerUser(any())
}
@Test
fun `회원가입 실패 - 요청 본문 누락 (400 Bad Request)`() {
val invalidRequest = """{"password": "password123"}""" // username 없음
mockMvc.perform(
post("/api/users/register")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidRequest)
)
.andExpect(status().isBadRequest)
}
}

백엔드를 배우는 입장에서, 백엔드 CRUD와 API, 그리고 테스트까지 모두 정상 작동한다는 것만으로도 감격스럽다... ㅠㅠ
상당 부분 GPT의 도움을 받아서 모든 코드가 내 것이라고 할 수는 없지만, 언제나 그랬듯 '구현'보다는 '이해와 복습'을 통해 내 것으로 만드는 데 목적을 둔 프로젝트이다.
그럼에도 포기하지 않고 어떻게든 끝까지 구현해보고 싶은 욕심이 있다.
이제 프론트코드를 작성해봐야한다.
화면 설계 (Figma)
주요 화면을 Figma로 디자인한다.
각 화면에서 필요한 UI 요소를 정의하고, Next.js 컴포넌트로 변환할 수 있도록 준비한다.
회원가입/로그인 페이지 구현
axios를 사용하여 백엔드와 통신하고, 회원가입과 로그인 기능을 구현.
로그인 후 JWT 토큰을 로컬 스토리지에 저장하여 인증을 유지한다.
게시판 페이지 구현
게시글 목록을 가져오는 API를 호출하여 화면에 게시글을 표시한다.
게시글 클릭 시, 해당 게시글의 상세 정보를 보여주는 페이지를 구현한다.
댓글 기능 구현
댓글 작성 및 조회 기능을 구현한다.
댓글은 게시글 상세 페이지에서 작성할 수 있게 된다.
배포 및 최종 테스트
백엔드: Spring Boot 애플리케이션을 빌드하여 서버에 배포한다.
프론트엔드: Next.js 애플리케이션을 빌드하고 배포한다.
로컬 및 테스트 환경에서 모든 기능이 정상적으로 작동하는지 확인한다.