diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
new file mode 100644
index 00000000..c78e69b3
--- /dev/null
+++ b/.github/workflows/ci-cd.yml
@@ -0,0 +1,49 @@
+name: CI/CD to EC2
+
+on:
+ push:
+ branches: [ "조은성/main", "조은성/6주차" ]
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Docker login
+ run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build and Push Docker Image
+ run: |
+ docker buildx build \
+ --platform linux/amd64,linux/arm64 \
+ -t ${{ secrets.DOCKER_USERNAME }}/blog-app:latest \
+ --push .
+
+ - name: Decode PEM key
+ run: |
+ echo "${{ secrets.EC2_KEY }}" | base64 -d > ec2-key.pem
+ chmod 600 ec2-key.pem
+
+ - name: Deploy to EC2
+ run: |
+ ssh -o StrictHostKeyChecking=no -i ec2-key.pem ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} << 'EOF'
+ docker pull ${{ secrets.DOCKER_USERNAME }}/blog-app:latest
+
+ docker stop blog-app || true
+ docker rm blog-app || true
+
+ cd ~
+ echo "KAKAO_CLIENT_ID=abc123" > .env
+ echo "KAKAO_CLIENT_SECRET=xyz456" >> .env
+
+ docker run -d -p 8080:8080 \
+ --name blog-app \
+ --env-file .env \
+ ${{ secrets.DOCKER_USERNAME }}/blog-app:latest
+ EOF
diff --git a/.gitignore b/.gitignore
index 42c1ad51..7fb273ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,6 +38,6 @@ out/
# 환경 설정 및 민감 파일
.env
-application.properties
+docker/.env
*.key
*.pem
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..772e1f72
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,9 @@
+FROM gradle:8.4.0-jdk21 as builder
+WORKDIR /app
+COPY . .
+RUN ./gradlew bootJar --no-daemon
+
+FROM eclipse-temurin:21-jdk
+WORKDIR /app
+COPY --from=builder /app/build/libs/*.jar app.jar
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/README.md b/README.md
index 40e33870..7acf0f96 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-## 프로젝트 구조
+## 프로젝트 구조
```
├── README.md
├── build.gradle
@@ -38,11 +38,9 @@
│ │ │ │ └── service
│ │ └── global
│ │ ├── auth
- │ │ ├── common
│ │ │ └── request
│ │ │ └── response
- │ │ ├── config
- │ │ └── exception
+ │ │ └── config
│ └── resources
│ └── application.properties
└── test
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 00000000..d5ed70a0
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,11 @@
+version: '3.8'
+
+services:
+ blog:
+ image: choes0101/blog-app:latest
+ container_name: blog-app
+ ports:
+ - "8080:8080"
+ env_file:
+ - .env
+ restart: always
diff --git a/src/main/java/com/blog/domain/comment/repository/CommentRepository.java b/src/main/java/com/blog/domain/comment/repository/CommentRepository.java
index 2cb1c151..fcebe34d 100644
--- a/src/main/java/com/blog/domain/comment/repository/CommentRepository.java
+++ b/src/main/java/com/blog/domain/comment/repository/CommentRepository.java
@@ -66,4 +66,10 @@ private RowMapper commentRowMapper() {
rs.getTimestamp("updated_at").toLocalDateTime()
);
}
+
+ public List findByAuthorId(UUID authorId) {
+ String sql = "SELECT * FROM comment WHERE author_id = ?";
+ return jdbcTemplate.query(sql, commentRowMapper(), authorId.toString());
+ }
+
}
diff --git a/src/main/java/com/blog/domain/comment/service/CommentService.java b/src/main/java/com/blog/domain/comment/service/CommentService.java
index eca0ebbd..f4de874a 100644
--- a/src/main/java/com/blog/domain/comment/service/CommentService.java
+++ b/src/main/java/com/blog/domain/comment/service/CommentService.java
@@ -54,4 +54,12 @@ public void updateComment(UUID id, CommentRequest request) {
public void deleteComment(UUID id) {
commentRepository.deleteById(id);
}
+
+ public List getCommentsByAuthor(UUID authorId) {
+ return commentRepository.findByAuthorId(authorId)
+ .stream()
+ .map(CommentResponse::from)
+ .collect(Collectors.toList());
+ }
+
}
diff --git a/src/main/java/com/blog/domain/image/Image.java b/src/main/java/com/blog/domain/image/Image.java
deleted file mode 100644
index 0da3b162..00000000
--- a/src/main/java/com/blog/domain/image/Image.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.blog.domain.image;
-
-public class Image {
-
-}
diff --git a/src/main/java/com/blog/domain/image/controller/ImageController.java b/src/main/java/com/blog/domain/image/controller/ImageController.java
new file mode 100644
index 00000000..ada6b212
--- /dev/null
+++ b/src/main/java/com/blog/domain/image/controller/ImageController.java
@@ -0,0 +1,23 @@
+package com.blog.domain.image.controller;
+
+import com.blog.domain.image.dto.ImageResponse;
+import com.blog.domain.image.service.ImageService;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+@RestController
+@RequestMapping("/images")
+public class ImageController {
+ private final ImageService imageService;
+
+ public ImageController(ImageService imageService) {
+ this.imageService = imageService;
+ }
+
+ @PostMapping("/upload")
+ public ResponseEntity upload(@RequestParam("file") MultipartFile file) {
+ ImageResponse response = imageService.upload(file);
+ return ResponseEntity.ok(response);
+ }
+}
diff --git a/src/main/java/com/blog/domain/image/domain/Image.java b/src/main/java/com/blog/domain/image/domain/Image.java
new file mode 100644
index 00000000..edcf2aa7
--- /dev/null
+++ b/src/main/java/com/blog/domain/image/domain/Image.java
@@ -0,0 +1,40 @@
+package com.blog.domain.image.domain;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+public class Image {
+ private UUID id;
+ private String originalName;
+ private String storedName;
+ private String url;
+ private LocalDateTime createdAt;
+
+ public Image(UUID id, String originalName, String storedName, String url, LocalDateTime createdAt) {
+ this.id = id;
+ this.originalName = originalName;
+ this.storedName = storedName;
+ this.url = url;
+ this.createdAt = createdAt;
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public String getOriginalName() {
+ return originalName;
+ }
+
+ public String getStoredName() {
+ return storedName;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+}
diff --git a/src/main/java/com/blog/domain/image/dto/ImageResponse.java b/src/main/java/com/blog/domain/image/dto/ImageResponse.java
new file mode 100644
index 00000000..f4566e9c
--- /dev/null
+++ b/src/main/java/com/blog/domain/image/dto/ImageResponse.java
@@ -0,0 +1,27 @@
+package com.blog.domain.image.dto;
+
+import com.blog.domain.image.domain.Image;
+
+import java.util.UUID;
+
+public class ImageResponse {
+ private UUID id;
+ private String url;
+
+ public ImageResponse(UUID id, String url) {
+ this.id = id;
+ this.url = url;
+ }
+
+ public static ImageResponse from(Image image) {
+ return new ImageResponse(image.getId(), image.getUrl());
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+}
diff --git a/src/main/java/com/blog/domain/image/repository/ImageRepository.java b/src/main/java/com/blog/domain/image/repository/ImageRepository.java
new file mode 100644
index 00000000..ba133b9d
--- /dev/null
+++ b/src/main/java/com/blog/domain/image/repository/ImageRepository.java
@@ -0,0 +1,45 @@
+package com.blog.domain.image.repository;
+
+import com.blog.domain.image.domain.Image;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Repository;
+
+import java.sql.Timestamp;
+import java.util.Optional;
+import java.util.UUID;
+
+@Repository
+public class ImageRepository {
+ private final JdbcTemplate jdbcTemplate;
+
+ public ImageRepository(JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ public void save(Image image) {
+ String sql = "INSERT INTO image (id, original_name, stored_name, url, created_at) VALUES (?, ?, ?, ?, ?)";
+ jdbcTemplate.update(sql,
+ image.getId().toString(),
+ image.getOriginalName(),
+ image.getStoredName(),
+ image.getUrl(),
+ Timestamp.valueOf(image.getCreatedAt()));
+ }
+
+ public Optional findById(UUID id) {
+ String sql = "SELECT * FROM image WHERE id = ?";
+ return jdbcTemplate.query(sql, rowMapper(), id.toString())
+ .stream().findFirst();
+ }
+
+ private RowMapper rowMapper() {
+ return (rs, rowNum) -> new Image(
+ UUID.fromString(rs.getString("id")),
+ rs.getString("original_name"),
+ rs.getString("stored_name"),
+ rs.getString("url"),
+ rs.getTimestamp("created_at").toLocalDateTime()
+ );
+ }
+}
diff --git a/src/main/java/com/blog/domain/image/service/ImageService.java b/src/main/java/com/blog/domain/image/service/ImageService.java
new file mode 100644
index 00000000..f98fbea7
--- /dev/null
+++ b/src/main/java/com/blog/domain/image/service/ImageService.java
@@ -0,0 +1,49 @@
+package com.blog.domain.image.service;
+
+import com.blog.domain.image.domain.Image;
+import com.blog.domain.image.dto.ImageResponse;
+import com.blog.domain.image.repository.ImageRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Service
+public class ImageService {
+ private static final String UPLOAD_DIR = System.getProperty("user.dir") + "/uploads/";
+ private final ImageRepository imageRepository;
+
+ public ImageService(ImageRepository imageRepository) {
+ this.imageRepository = imageRepository;
+ }
+ public ImageResponse upload(MultipartFile file) {
+ try {
+ String originalName = file.getOriginalFilename();
+ String ext = originalName.substring(originalName.lastIndexOf("."));
+ String storedName = UUID.randomUUID() + ext;
+ Path storedPath = Path.of(UPLOAD_DIR + storedName);
+
+ Files.createDirectories(storedPath.getParent()); // 디렉토리 없으면 생성
+ file.transferTo(storedPath.toFile());
+
+ String url = "/images/" + storedName;
+
+ Image image = new Image(
+ UUID.randomUUID(),
+ originalName,
+ storedName,
+ url,
+ LocalDateTime.now()
+ );
+
+ imageRepository.save(image);
+ return ImageResponse.from(image);
+ } catch (Exception e) {
+ throw new RuntimeException("이미지 저장 실패", e);
+ }
+ }
+}
diff --git a/src/main/java/com/blog/domain/post/repository/PostRepository.java b/src/main/java/com/blog/domain/post/repository/PostRepository.java
index f3f90bc3..eaae4eda 100644
--- a/src/main/java/com/blog/domain/post/repository/PostRepository.java
+++ b/src/main/java/com/blog/domain/post/repository/PostRepository.java
@@ -67,4 +67,10 @@ private RowMapper postRowMapper() {
rs.getTimestamp("updated_at").toLocalDateTime()
);
}
+
+ public List findByAuthorId(String authorId) {
+ String sql = "SELECT * FROM post WHERE author_id = ?";
+ return jdbcTemplate.query(sql, postRowMapper(), authorId);
+ }
+
}
diff --git a/src/main/java/com/blog/domain/post/service/PostService.java b/src/main/java/com/blog/domain/post/service/PostService.java
index 044d8313..eef4b079 100644
--- a/src/main/java/com/blog/domain/post/service/PostService.java
+++ b/src/main/java/com/blog/domain/post/service/PostService.java
@@ -64,4 +64,12 @@ public void updatePost(UUID id, PostRequest request) {
public void deletePost(UUID id) {
postRepository.deleteById(id);
}
+
+ public List getPostsByAuthor(UUID authorId) {
+ return postRepository.findByAuthorId(authorId.toString())
+ .stream()
+ .map(PostResponse::from)
+ .collect(Collectors.toList());
+ }
+
}
diff --git a/src/main/java/com/blog/domain/user/controller/UserController.java b/src/main/java/com/blog/domain/user/controller/UserController.java
index 52eefa15..c1d43d76 100644
--- a/src/main/java/com/blog/domain/user/controller/UserController.java
+++ b/src/main/java/com/blog/domain/user/controller/UserController.java
@@ -1,18 +1,33 @@
package com.blog.domain.user.controller;
+import com.blog.domain.comment.dto.CommentResponse;
+import com.blog.domain.comment.service.CommentService;
+import com.blog.domain.post.dto.PostResponse;
+import com.blog.domain.post.service.PostService;
import com.blog.domain.user.domain.User;
+import com.blog.domain.user.dto.UpdateProfileImageRequest;
+import com.blog.domain.user.dto.UserProfileResponse;
+import com.blog.domain.user.service.UserService;
import com.blog.global.auth.service.AuthService;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestHeader;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
@RestController
public class UserController {
private final AuthService authService;
+ private final UserService userService;
+ private final PostService postService;
+ private final CommentService commentService;
- public UserController(AuthService authService) {
+ public UserController(AuthService authService, UserService userService , PostService postService , CommentService commentService) {
this.authService = authService;
+ this.userService = userService;
+ this.postService = postService;
+ this.commentService = commentService;
}
@GetMapping("/protected-resource")
@@ -21,5 +36,58 @@ public ResponseEntity getProtectedResource(
User user = authService.getUserInfoFromToken(token);
return ResponseEntity.ok(user);
}
-}
+ @GetMapping("/me")
+ public ResponseEntity getMyInfo(
+ @RequestHeader("Authorization") String token) {
+ User user = getUserFromToken(token);
+ return ResponseEntity.ok(UserProfileResponse.from(user));
+ }
+
+ @DeleteMapping("/me")
+ public ResponseEntity