Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
dfb73e5
feat: 회원가입 API 추가
yooniicode May 17, 2025
2aed0d8
fix: swagger 관련 오류 수정
yooniicode May 17, 2025
9b526ac
Merge pull request #27 from NeodinaryHackathon-teamE/feat/#21
wlswnsdn May 17, 2025
3441aeb
chore: merging develop
yooniicode May 17, 2025
e807f0d
chore: role 제거
yooniicode May 17, 2025
0af13fb
[feat] 제보글 작성 진행중
joochaeyeon May 17, 2025
72f1155
feat: 로그인 추가
May 17, 2025
4a312ef
[feat] 좋아요 기능 추가
joochaeyeon May 17, 2025
c4711ac
feat: 로그인, 회원가입 완
May 17, 2025
ab77ca7
Merge pull request #29 from NeodinaryHackathon-teamE/feat/#22
wlswnsdn May 17, 2025
6fee459
Merge branch 'develop' of https://github.com/NeodinaryHackathon-teamE…
joochaeyeon May 17, 2025
8230135
feat: post 카테고리별 조회
yooniicode May 17, 2025
7a63e7b
feat: post 카테고리별 조회
yooniicode May 17, 2025
ce20690
feat: status별로 제보글 조회 api 설계
yooniicode May 17, 2025
ec166e5
[feat] 게시글
joochaeyeon May 17, 2025
336ea09
[feat] 좋아요 틀 구성
joochaeyeon May 17, 2025
93b833d
Merge pull request #28 from NeodinaryHackathon-teamE/feat/#5
joochaeyeon May 17, 2025
bbafe46
feat: mypage 관련 API 설계
yooniicode May 17, 2025
aef125c
Merge branch 'develop' into feat/#32
yooniicode May 17, 2025
f8b6045
feat: mypage 조회 API 설꼐
yooniicode May 17, 2025
ef98e33
chore: merge conflicts
yooniicode May 17, 2025
cf8f431
Merge pull request #31 from NeodinaryHackathon-teamE/feat/#30
yooniicode May 17, 2025
50aaf12
Merge branch 'develop' into feat/#32
yooniicode May 17, 2025
519bf26
Merge pull request #36 from NeodinaryHackathon-teamE/feat/#32
yooniicode May 17, 2025
1248f47
feat: 좋아요 누르기
May 17, 2025
3f17232
docs: Update README.md
yooniicode May 17, 2025
cab4be7
feat: 좋아요 누르기/취소 완
May 17, 2025
8e9582d
feat: swagger 에러코드 동적 문서화
yooniicode May 17, 2025
8708aef
feat: 내가 좋아요 누른 글 조회 완
May 17, 2025
923c4cb
[feat] 게시물에 사진 업로드 기능구현
joochaeyeon May 17, 2025
16775aa
Merge branch 'develop' into feat/#34
wlswnsdn May 17, 2025
7155a69
chore: 오타 수정 및 포트:8080
May 17, 2025
be3201c
Merge branch 'develop' into feat/#35
joochaeyeon May 17, 2025
0cb61e6
Merge pull request #39 from NeodinaryHackathon-teamE/feat/#35
joochaeyeon May 17, 2025
fb3bf20
Merge branch 'feat/#34' of https://github.com/NeodinaryHackathon-team…
May 17, 2025
a7bbec8
docs: errorstatus 수정
yooniicode May 17, 2025
5be1a34
Merge pull request #40 from NeodinaryHackathon-teamE/feat/#34
wlswnsdn May 17, 2025
72e8dd6
[fix] 충돌 해결
joochaeyeon May 17, 2025
6cc807d
Merge pull request #41 from NeodinaryHackathon-teamE/feat/#35
joochaeyeon May 17, 2025
4bb4225
feat: 내 제보글 조회
May 17, 2025
27e2d8c
Merge branch 'develop' of https://github.com/NeodinaryHackathon-teamE…
May 17, 2025
2abad0f
[feat] 이미지 무조건 2개 올려야한다는 세부조건 기능 구현
joochaeyeon May 17, 2025
e061c10
Merge pull request #44 from NeodinaryHackathon-teamE/feat/#42
joochaeyeon May 17, 2025
6088ff9
Merge pull request #43 from NeodinaryHackathon-teamE/feat/#33
wlswnsdn May 17, 2025
7d570a8
fix: swagger custom 어노테이션 오류코드 반환 안되는 에러 수정
yooniicode May 17, 2025
e77c525
chore: merge conflicts
yooniicode May 17, 2025
022b397
Merge pull request #45 from NeodinaryHackathon-teamE/feat/#37
yooniicode May 17, 2025
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
15 changes: 10 additions & 5 deletions Neo_backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ plugins {
id 'io.spring.dependency-management' version '1.1.7'
}

dependencyManagement {
imports {
mavenBom "org.springframework.boot:spring-boot-dependencies:3.4.5"
}
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

Expand All @@ -29,27 +35,26 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java:8.0.33'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
implementation 'org.springframework.boot:spring-boot-starter'
// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'

implementation 'javax.xml.bind:jaxb-api:2.3.1'
implementation 'org.glassfish.jaxb:jaxb-runtime:2.3.1'

//Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final'

// AWS S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

}


tasks.named('test') {
useJUnitPlatform()
}
Binary file added Neo_backend/image1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.neo_backend.domain.image.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.web.multipart.MultipartFile;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ImageRequest {

private MultipartFile imageFile;

public MultipartFile getImageFile() {
return imageFile;
}

public void setImageFile(MultipartFile imageFile) {
this.imageFile = imageFile;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.neo_backend.domain.image.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ImageResponse {
private Long imageId;
private Long postId;
private String imageUrl;

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@
import com.example.neo_backend.domain.pin.entity.Pin;
import com.example.neo_backend.domain.post.entity.Post;
import com.example.neo_backend.domain.user.entity.User;
import com.example.neo_backend.global.common.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

@Entity
public class Image {
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Image extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long imageId;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.neo_backend.domain.image.repository;

import com.example.neo_backend.domain.image.entity.Image;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ImageRepository extends JpaRepository<Image, Long> {
List<Image> findByPost_PostId(Long postId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.example.neo_backend.domain.image.service;

import com.example.neo_backend.domain.image.entity.Image;
import com.example.neo_backend.domain.image.repository.ImageRepository;
import com.example.neo_backend.domain.image.utils.S3Uploader;
import com.example.neo_backend.domain.pin.entity.Pin;
import com.example.neo_backend.domain.post.entity.Post;
import com.example.neo_backend.domain.post.repository.PostRepository;
import com.example.neo_backend.domain.user.entity.User;
import com.example.neo_backend.global.common.status.ErrorStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class ImageService {
private final S3Uploader s3Uploader;
private final ImageRepository imageRepository;
private final PostRepository postRepository;

@Transactional
public List<Image> uploadImages(Long postId, List<MultipartFile> images, User user, Pin pin) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new RuntimeException(
ErrorStatus._NOT_FOUND.getMessageOrDefault("해당하는 게시글이 없습니다.")));

List<Image> savedImages = new ArrayList<>();

for (MultipartFile imageFile : images) {
try {
System.out.println("Uploading image: " + imageFile.getOriginalFilename() + ", size: " + imageFile.getSize());
String imageUrl = s3Uploader.uploadFiles(imageFile, "uploads");
System.out.println("Uploaded URL: " + imageUrl);

Image image = Image.builder()
.post(post)
.user(user)
.pin(pin)
.imageURL(imageUrl)
.build();

savedImages.add(imageRepository.save(image));
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(
ErrorStatus._INTERNAL_SERVER_ERROR.getMessageOrDefault("이미지 업로드 실패"), e);
}
}

return savedImages;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.example.neo_backend.domain.image.utils;

import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.example.neo_backend.global.common.status.ErrorStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;

@Component
@RequiredArgsConstructor
public class S3Uploader {
private final AmazonS3Client amazonS3Client;

@Value("${cloud.aws.s3.bucket}")
private String bucket;

// MultipartFile -> File 변환 후 S3 업로드, URL 반환
public String uploadFiles(MultipartFile multipartFile, String dirName) {
File uploadFile = convert(multipartFile)
.orElseThrow(() -> new RuntimeException(
ErrorStatus.VALIDATION_ERROR.getMessageOrDefault("MultipartFile 변환 실패")));

return upload(uploadFile, dirName);
}

// 실제 S3 업로드
public String upload(File uploadFile, String filePath) {
try {
String fileName = filePath + "/" + UUID.randomUUID() + "-" + uploadFile.getName();
String uploadImageUrl = putS3(uploadFile, fileName);

// 업로드 후 로컬 파일 삭제 (필요시)
if (uploadFile.delete()) {
System.out.println("임시 파일 삭제 성공");
} else {
System.out.println("임시 파일 삭제 실패");
}

return uploadImageUrl;
} catch (Exception e) {
throw new RuntimeException(
ErrorStatus._INTERNAL_SERVER_ERROR.getMessageOrDefault("파일 업로드 중 오류 발생"), e);
}
}

private String putS3(File uploadFile, String fileName) {
amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadFile));
return amazonS3Client.getUrl(bucket, fileName).toString();
}

// MultipartFile -> File 변환
private Optional<File> convert(MultipartFile file) {
try {
// 임시파일을 OS temp폴더에 고유한 이름으로 생성
String originalFilename = file.getOriginalFilename();
String suffix = "";
if (originalFilename != null && originalFilename.contains(".")) {
suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
}

File convertFile = File.createTempFile("tempfile-", UUID.randomUUID() + suffix);
try (FileOutputStream fos = new FileOutputStream(convertFile)) {
fos.write(file.getBytes());
}
return Optional.of(convertFile);
} catch (IOException e) {
e.printStackTrace();
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.example.neo_backend.domain.like.controller;
import com.example.neo_backend.domain.like.dto.LikeResponseDto;
import com.example.neo_backend.domain.like.service.LikesService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/likes")
@Tag(name = "likes", description = "좋아요 관련 API")
@Slf4j
public class LikesController {

private final LikesService likesService;

@PostMapping("")
@Operation(summary = "좋아요 등록", description = "제보글에 좋아요를 등록합니다.")
public ResponseEntity<Long> createLike(@RequestParam Long postId, HttpServletRequest request) {
log.info("좋아요 등록 요청 postId: {}", postId);
Long like = likesService.createLike(postId, request);
return ResponseEntity.ok(like);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.neo_backend.domain.like.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class LikeResponseDto {
private Long likeId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.example.neo_backend.domain.pin.entity.Pin;
import com.example.neo_backend.domain.post.entity.Post;
import com.example.neo_backend.domain.user.entity.User;
import com.example.neo_backend.global.common.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

Expand All @@ -11,7 +12,7 @@
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Like {
public class Likes extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand All @@ -31,4 +32,12 @@ public class Like {

@Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT false")
private Boolean isLiked;
}

public void cancelLike() {
this.isLiked = false;
}

public void doLike() {
this.isLiked = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.neo_backend.domain.like.repository;

import com.example.neo_backend.domain.like.entity.Likes;
import com.example.neo_backend.domain.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface LikesRepository extends JpaRepository<Likes, Long> {
Long countByPostPostIdAndIsLikedTrue(Long postId);
int countByUser(User user);

@Query("SELECT l.post.postId FROM Likes l WHERE l.user.userId = :userId AND l.isLiked = true")
List<Long> findLikedPostIdsByUserId(@Param("userId") Long userId);

Optional<Likes> findByPostPostIdAndUserUserId(Long postId, Long userId);
}
Loading