diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..73cab569 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,45 @@ +name: Deploy to Amazon EC2 + +on: + push: + branches: [ "develop" ] + +jobs: + deploy: + name: deploy + runs-on: ubuntu-latest + environment: production + + steps: + - name: setting-jdk17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'zulu' + + - name: checkout + uses: actions/checkout@v2 +# +# - name: test +# run: ./gradlew clean test + + - name : build + run: ./gradlew build -x test + + - name: copy file to Develop server via ssh key + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.AWS_HOST }} + username: ${{ secrets.AWS_USERNAME }} + key: ${{ secrets.AWS_SSH_KEY }} + source: "build/libs/*" + target: "/home/ubuntu/apiserver" + + - name: deploy + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.AWS_HOST }} + username: ${{ secrets.AWS_USERNAME }} + key: ${{ secrets.AWS_SSH_KEY }} + script: | + sudo systemctl restart apiserver.service diff --git a/.github/workflows/prtest.yml b/.github/workflows/prtest.yml new file mode 100644 index 00000000..2b5ea150 --- /dev/null +++ b/.github/workflows/prtest.yml @@ -0,0 +1,21 @@ +name: PR TEST + +on: + pull_request: + branches: [ "develop", "main" ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: setting-jdk17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'zulu' + + - name: checkout + uses: actions/checkout@v2 + + - name: execute TestCode + run: ./gradlew clean test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4c75e424..ab064487 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ out/ ### VS Code ### .vscode/ +### .env ### .env \ No newline at end of file diff --git a/build.gradle b/build.gradle index d48d37f6..4bb87f13 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,8 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' + compileOnly 'org.projectlombok:lombok' // runtimeOnly 'com.mysql:mysql-connector-j' runtimeOnly 'com.h2database:h2' @@ -33,18 +35,82 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - // env 파일 읽도록 도와주는 라이브러리 + // WEB SOCKET + implementation 'org.springframework.boot:spring-boot-starter-websocket' + + // MONGO DB + implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' + + // ENV implementation 'me.paulschwarz:spring-dotenv:4.0.0' + + // VALIDATION + implementation 'org.springframework.boot:spring-boot-starter-validation' + + // SECURITY + implementation 'org.springframework.boot:spring-boot-starter-security' + + // SMTP + implementation 'org.springframework.boot:spring-boot-starter-mail' + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // Query DSL + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + // 인메모리 몽고디비 flapdoodle + testImplementation 'de.flapdoodle.embed:de.flapdoodle.embed.mongo:4.6.0' + + // OAUTH2 + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // MOCKITO + testImplementation "org.mockito:mockito-core:3.+" + + //WIREMOCK (외부 의존성 테스트용) + implementation 'org.wiremock.integrations:wiremock-spring-boot:3.3.0' + + // ModelMapper 사용 + //객체 간 매핑 처리 + implementation 'org.modelmapper:modelmapper:3.1.0' + + // swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' + + // S3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' } tasks.named('test') { useJUnitPlatform() } +def querydslDir = "build/generated/querydsl" + +sourceSets { + main.java.srcDirs += [ querydslDir ] +} + +tasks.withType(JavaCompile) { + options.getGeneratedSourceOutputDirectory().set(file(querydslDir)) +} + +clean.doLast { + file(querydslDir).deleteDir() +} + sourceSets { test { java { srcDirs = ['src/test/java'] } } -} +} \ No newline at end of file diff --git a/gradlew b/gradlew index f5feea6d..b26d4110 100755 --- a/gradlew +++ b/gradlew @@ -249,4 +249,4 @@ eval "set -- $( tr '\n' ' ' )" '"$@"' -exec "$JAVACMD" "$@" +exec "$JAVACMD" "$@" \ No newline at end of file diff --git a/src/main/generated/com/example/api/account/entity/QLocation.java b/src/main/generated/com/example/api/account/entity/QLocation.java new file mode 100644 index 00000000..c15f6d8b --- /dev/null +++ b/src/main/generated/com/example/api/account/entity/QLocation.java @@ -0,0 +1,43 @@ +package com.example.api.account.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QLocation is a Querydsl query type for Location + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QLocation extends EntityPathBase { + + private static final long serialVersionUID = -590127238L; + + public static final QLocation location = new QLocation("location"); + + public final StringPath address = createString("address"); + + public final StringPath detailAddress = createString("detailAddress"); + + public final NumberPath id = createNumber("id", Long.class); + + public final StringPath zipcode = createString("zipcode"); + + public QLocation(String variable) { + super(Location.class, forVariable(variable)); + } + + public QLocation(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QLocation(PathMetadata metadata) { + super(Location.class, metadata); + } + +} + diff --git a/src/main/generated/com/example/api/auth/entitiy/QRefreshToken.java b/src/main/generated/com/example/api/auth/entitiy/QRefreshToken.java new file mode 100644 index 00000000..ae3f4b01 --- /dev/null +++ b/src/main/generated/com/example/api/auth/entitiy/QRefreshToken.java @@ -0,0 +1,57 @@ +package com.example.api.auth.entitiy; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QRefreshToken is a Querydsl query type for RefreshToken + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QRefreshToken extends EntityPathBase { + + private static final long serialVersionUID = -1655386265L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QRefreshToken refreshToken1 = new QRefreshToken("refreshToken1"); + + public final NumberPath id = createNumber("id", Long.class); + + public final BooleanPath isExpired = createBoolean("isExpired"); + + public final DateTimePath recentLogin = createDateTime("recentLogin", java.time.LocalDateTime.class); + + public final StringPath refreshToken = createString("refreshToken"); + + public final com.example.api.domain.QAccount user; + + public QRefreshToken(String variable) { + this(RefreshToken.class, forVariable(variable), INITS); + } + + public QRefreshToken(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QRefreshToken(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QRefreshToken(PathMetadata metadata, PathInits inits) { + this(RefreshToken.class, metadata, inits); + } + + public QRefreshToken(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.user = inits.isInitialized("user") ? new com.example.api.domain.QAccount(forProperty("user")) : null; + } + +} + diff --git a/src/main/generated/com/example/api/domain/QAccount.java b/src/main/generated/com/example/api/domain/QAccount.java new file mode 100644 index 00000000..bebfa199 --- /dev/null +++ b/src/main/generated/com/example/api/domain/QAccount.java @@ -0,0 +1,78 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QAccount is a Querydsl query type for Account + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QAccount extends EntityPathBase { + + private static final long serialVersionUID = -1087167288L; + + public static final QAccount account = new QAccount("account"); + + public final QBaseEntity _super = new QBaseEntity(this); + + public final NumberPath accountId = createNumber("accountId", Long.class); + + public final NumberPath age = createNumber("age", Integer.class); + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + public final BooleanPath deleted = createBoolean("deleted"); + + public final StringPath email = createString("email"); + + public final BooleanPath emailReceivable = createBoolean("emailReceivable"); + + public final StringPath loginId = createString("loginId"); + + public final StringPath name = createString("name"); + + public final EnumPath nationality = createEnum("nationality", com.example.api.account.entity.Nationality.class); + + public final StringPath nickname = createString("nickname"); + + public final BooleanPath openStatus = createBoolean("openStatus"); + + public final StringPath password = createString("password"); + + public final StringPath phoneNumber = createString("phoneNumber"); + + public final StringPath profileImage = createString("profileImage"); + + public final CollectionPath> roles = this.>createCollection("roles", com.example.api.account.entity.UserRole.class, EnumPath.class, PathInits.DIRECT2); + + public final StringPath sex = createString("sex"); + + public final NumberPath starPoint = createNumber("starPoint", Float.class); + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public final NumberPath workCount = createNumber("workCount", Integer.class); + + public QAccount(String variable) { + super(Account.class, forVariable(variable)); + } + + public QAccount(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QAccount(PathMetadata metadata) { + super(Account.class, metadata); + } + +} + diff --git a/src/main/generated/com/example/api/domain/QAnnouncement.java b/src/main/generated/com/example/api/domain/QAnnouncement.java new file mode 100644 index 00000000..2280f648 --- /dev/null +++ b/src/main/generated/com/example/api/domain/QAnnouncement.java @@ -0,0 +1,53 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QAnnouncement is a Querydsl query type for Announcement + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QAnnouncement extends EntityPathBase { + + private static final long serialVersionUID = 1512577932L; + + public static final QAnnouncement announcement = new QAnnouncement("announcement"); + + public final QBaseEntity _super = new QBaseEntity(this); + + public final StringPath announcementContent = createString("announcementContent"); + + public final NumberPath announcementId = createNumber("announcementId", Long.class); + + public final StringPath announcementTitle = createString("announcementTitle"); + + public final StringPath announcementType = createString("announcementType"); + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public final NumberPath viewCount = createNumber("viewCount", Integer.class); + + public QAnnouncement(String variable) { + super(Announcement.class, forVariable(variable)); + } + + public QAnnouncement(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QAnnouncement(PathMetadata metadata) { + super(Announcement.class, metadata); + } + +} + diff --git a/src/main/generated/com/example/api/domain/QBaseEntity.java b/src/main/generated/com/example/api/domain/QBaseEntity.java new file mode 100644 index 00000000..09c42891 --- /dev/null +++ b/src/main/generated/com/example/api/domain/QBaseEntity.java @@ -0,0 +1,39 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QBaseEntity is a Querydsl query type for BaseEntity + */ +@Generated("com.querydsl.codegen.DefaultSupertypeSerializer") +public class QBaseEntity extends EntityPathBase { + + private static final long serialVersionUID = 127095193L; + + public static final QBaseEntity baseEntity = new QBaseEntity("baseEntity"); + + public final DateTimePath createdDate = createDateTime("createdDate", java.time.LocalDateTime.class); + + public final DateTimePath updatedDate = createDateTime("updatedDate", java.time.LocalDateTime.class); + + public QBaseEntity(String variable) { + super(BaseEntity.class, forVariable(variable)); + } + + public QBaseEntity(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QBaseEntity(PathMetadata metadata) { + super(BaseEntity.class, metadata); + } + +} + diff --git a/src/main/generated/com/example/api/domain/QBusiness.java b/src/main/generated/com/example/api/domain/QBusiness.java new file mode 100644 index 00000000..e3b8979c --- /dev/null +++ b/src/main/generated/com/example/api/domain/QBusiness.java @@ -0,0 +1,72 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QBusiness is a Querydsl query type for Business + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QBusiness extends EntityPathBase { + + private static final long serialVersionUID = 1647868037L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QBusiness business = new QBusiness("business"); + + public final QBaseEntity _super = new QBaseEntity(this); + + public final ListPath businessCategories = this.createList("businessCategories", BusinessCategory.class, QBusinessCategory.class, PathInits.DIRECT2); + + public final NumberPath businessId = createNumber("businessId", Long.class); + + public final StringPath businessName = createString("businessName"); + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + public final QAccount employer; + + public final com.example.api.account.entity.QLocation location; + + public final DatePath openDate = createDate("openDate", java.time.LocalDate.class); + + public final StringPath registrationNumber = createString("registrationNumber"); + + public final StringPath representationName = createString("representationName"); + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public QBusiness(String variable) { + this(Business.class, forVariable(variable), INITS); + } + + public QBusiness(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QBusiness(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QBusiness(PathMetadata metadata, PathInits inits) { + this(Business.class, metadata, inits); + } + + public QBusiness(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.employer = inits.isInitialized("employer") ? new QAccount(forProperty("employer")) : null; + this.location = inits.isInitialized("location") ? new com.example.api.account.entity.QLocation(forProperty("location")) : null; + } + +} + diff --git a/src/main/generated/com/example/api/domain/QBusinessCategory.java b/src/main/generated/com/example/api/domain/QBusinessCategory.java new file mode 100644 index 00000000..6654e604 --- /dev/null +++ b/src/main/generated/com/example/api/domain/QBusinessCategory.java @@ -0,0 +1,62 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QBusinessCategory is a Querydsl query type for BusinessCategory + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QBusinessCategory extends EntityPathBase { + + private static final long serialVersionUID = -861191005L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QBusinessCategory businessCategory = new QBusinessCategory("businessCategory"); + + public final QBaseEntity _super = new QBaseEntity(this); + + public final QBusiness business; + + public final QCategory category; + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + public final NumberPath id = createNumber("id", Long.class); + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public QBusinessCategory(String variable) { + this(BusinessCategory.class, forVariable(variable), INITS); + } + + public QBusinessCategory(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QBusinessCategory(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QBusinessCategory(PathMetadata metadata, PathInits inits) { + this(BusinessCategory.class, metadata, inits); + } + + public QBusinessCategory(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.business = inits.isInitialized("business") ? new QBusiness(forProperty("business"), inits.get("business")) : null; + this.category = inits.isInitialized("category") ? new QCategory(forProperty("category"), inits.get("category")) : null; + } + +} + diff --git a/src/main/generated/com/example/api/domain/QCategory.java b/src/main/generated/com/example/api/domain/QCategory.java new file mode 100644 index 00000000..0bf67e23 --- /dev/null +++ b/src/main/generated/com/example/api/domain/QCategory.java @@ -0,0 +1,61 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QCategory is a Querydsl query type for Category + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QCategory extends EntityPathBase { + + private static final long serialVersionUID = -1449757245L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QCategory category = new QCategory("category"); + + public final QBaseEntity _super = new QBaseEntity(this); + + public final QAccount account; + + public final NumberPath categoryId = createNumber("categoryId", Long.class); + + public final StringPath categoryName = createString("categoryName"); + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public QCategory(String variable) { + this(Category.class, forVariable(variable), INITS); + } + + public QCategory(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QCategory(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QCategory(PathMetadata metadata, PathInits inits) { + this(Category.class, metadata, inits); + } + + public QCategory(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.account = inits.isInitialized("account") ? new QAccount(forProperty("account")) : null; + } + +} + diff --git a/src/main/generated/com/example/api/domain/QChatRoom.java b/src/main/generated/com/example/api/domain/QChatRoom.java new file mode 100644 index 00000000..b8542ca3 --- /dev/null +++ b/src/main/generated/com/example/api/domain/QChatRoom.java @@ -0,0 +1,53 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QChatRoom is a Querydsl query type for ChatRoom + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QChatRoom extends EntityPathBase { + + private static final long serialVersionUID = -62925544L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QChatRoom chatRoom = new QChatRoom("chatRoom"); + + public final NumberPath chatRoomId = createNumber("chatRoomId", Long.class); + + public final QOfferEmployment offerEmployment; + + public final DateTimePath suggestGeneratedDate = createDateTime("suggestGeneratedDate", java.time.LocalDateTime.class); + + public QChatRoom(String variable) { + this(ChatRoom.class, forVariable(variable), INITS); + } + + public QChatRoom(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QChatRoom(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QChatRoom(PathMetadata metadata, PathInits inits) { + this(ChatRoom.class, metadata, inits); + } + + public QChatRoom(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.offerEmployment = inits.isInitialized("offerEmployment") ? new QOfferEmployment(forProperty("offerEmployment"), inits.get("offerEmployment")) : null; + } + +} + diff --git a/src/main/generated/com/example/api/domain/QContract.java b/src/main/generated/com/example/api/domain/QContract.java new file mode 100644 index 00000000..59932355 --- /dev/null +++ b/src/main/generated/com/example/api/domain/QContract.java @@ -0,0 +1,67 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QContract is a Querydsl query type for Contract + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QContract extends EntityPathBase { + + private static final long serialVersionUID = -2067215913L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QContract contract = new QContract("contract"); + + public final QBaseEntity _super = new QBaseEntity(this); + + public final DateTimePath contractEndTime = createDateTime("contractEndTime", java.time.LocalDateTime.class); + + public final NumberPath contractHourlyPay = createNumber("contractHourlyPay", Integer.class); + + public final NumberPath contractId = createNumber("contractId", Long.class); + + public final DateTimePath contractStartTime = createDateTime("contractStartTime", java.time.LocalDateTime.class); + + public final BooleanPath contractSucceeded = createBoolean("contractSucceeded"); + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + public final QOfferEmployment offerEmployment; + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public QContract(String variable) { + this(Contract.class, forVariable(variable), INITS); + } + + public QContract(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QContract(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QContract(PathMetadata metadata, PathInits inits) { + this(Contract.class, metadata, inits); + } + + public QContract(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.offerEmployment = inits.isInitialized("offerEmployment") ? new QOfferEmployment(forProperty("offerEmployment"), inits.get("offerEmployment")) : null; + } + +} + diff --git a/src/main/generated/com/example/api/domain/QExternalCareer.java b/src/main/generated/com/example/api/domain/QExternalCareer.java new file mode 100644 index 00000000..0c112002 --- /dev/null +++ b/src/main/generated/com/example/api/domain/QExternalCareer.java @@ -0,0 +1,63 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QExternalCareer is a Querydsl query type for ExternalCareer + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QExternalCareer extends EntityPathBase { + + private static final long serialVersionUID = 606757934L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QExternalCareer externalCareer = new QExternalCareer("externalCareer"); + + public final QBaseEntity _super = new QBaseEntity(this); + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + public final QAccount employee; + + public final NumberPath id = createNumber("id", Long.class); + + public final StringPath Name = createString("Name"); + + public final StringPath period = createString("period"); + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public QExternalCareer(String variable) { + this(ExternalCareer.class, forVariable(variable), INITS); + } + + public QExternalCareer(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QExternalCareer(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QExternalCareer(PathMetadata metadata, PathInits inits) { + this(ExternalCareer.class, metadata, inits); + } + + public QExternalCareer(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.employee = inits.isInitialized("employee") ? new QAccount(forProperty("employee")) : null; + } + +} + diff --git a/src/main/generated/com/example/api/domain/QFlavored.java b/src/main/generated/com/example/api/domain/QFlavored.java new file mode 100644 index 00000000..a29bd767 --- /dev/null +++ b/src/main/generated/com/example/api/domain/QFlavored.java @@ -0,0 +1,62 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QFlavored is a Querydsl query type for Flavored + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QFlavored extends EntityPathBase { + + private static final long serialVersionUID = 128299138L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QFlavored flavored = new QFlavored("flavored"); + + public final QBaseEntity _super = new QBaseEntity(this); + + public final QCategory category; + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + public final QAccount employee; + + public final NumberPath flavoredId = createNumber("flavoredId", Long.class); + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public QFlavored(String variable) { + this(Flavored.class, forVariable(variable), INITS); + } + + public QFlavored(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QFlavored(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QFlavored(PathMetadata metadata, PathInits inits) { + this(Flavored.class, metadata, inits); + } + + public QFlavored(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.category = inits.isInitialized("category") ? new QCategory(forProperty("category"), inits.get("category")) : null; + this.employee = inits.isInitialized("employee") ? new QAccount(forProperty("employee")) : null; + } + +} + diff --git a/src/main/generated/com/example/api/domain/QInquiry.java b/src/main/generated/com/example/api/domain/QInquiry.java new file mode 100644 index 00000000..b1d0cfbb --- /dev/null +++ b/src/main/generated/com/example/api/domain/QInquiry.java @@ -0,0 +1,71 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QInquiry is a Querydsl query type for Inquiry + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QInquiry extends EntityPathBase { + + private static final long serialVersionUID = 2045912162L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QInquiry inquiry = new QInquiry("inquiry"); + + public final QBaseEntity _super = new QBaseEntity(this); + + public final DateTimePath answerDate = createDateTime("answerDate", java.time.LocalDateTime.class); + + public final StringPath content = createString("content"); + + public final QAccount createdBy; + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + public final NumberPath inquiryId = createNumber("inquiryId", Long.class); + + public final EnumPath inquiryStatus = createEnum("inquiryStatus", Inquiry.InquiryStatus.class); + + public final StringPath inquiryType = createString("inquiryType"); + + public final StringPath subInquiryType = createString("subInquiryType"); + + public final StringPath title = createString("title"); + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public QInquiry(String variable) { + this(Inquiry.class, forVariable(variable), INITS); + } + + public QInquiry(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QInquiry(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QInquiry(PathMetadata metadata, PathInits inits) { + this(Inquiry.class, metadata, inits); + } + + public QInquiry(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.createdBy = inits.isInitialized("createdBy") ? new QAccount(forProperty("createdBy")) : null; + } + +} + diff --git a/src/main/generated/com/example/api/domain/QOfferEmployment.java b/src/main/generated/com/example/api/domain/QOfferEmployment.java new file mode 100644 index 00000000..9f79f3ce --- /dev/null +++ b/src/main/generated/com/example/api/domain/QOfferEmployment.java @@ -0,0 +1,71 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QOfferEmployment is a Querydsl query type for OfferEmployment + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QOfferEmployment extends EntityPathBase { + + private static final long serialVersionUID = -82696445L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QOfferEmployment offerEmployment = new QOfferEmployment("offerEmployment"); + + public final QBusiness business; + + public final QContract contract; + + public final QAccount employee; + + public final DateTimePath suggestEndTime = createDateTime("suggestEndTime", java.time.LocalDateTime.class); + + public final BooleanPath suggestFinished = createBoolean("suggestFinished"); + + public final NumberPath suggestHourlyPay = createNumber("suggestHourlyPay", Integer.class); + + public final NumberPath suggestId = createNumber("suggestId", Long.class); + + public final BooleanPath suggestReaded = createBoolean("suggestReaded"); + + public final DateTimePath suggestRegisterTime = createDateTime("suggestRegisterTime", java.time.LocalDateTime.class); + + public final DateTimePath suggestStartTime = createDateTime("suggestStartTime", java.time.LocalDateTime.class); + + public final BooleanPath suggestSucceeded = createBoolean("suggestSucceeded"); + + public QOfferEmployment(String variable) { + this(OfferEmployment.class, forVariable(variable), INITS); + } + + public QOfferEmployment(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QOfferEmployment(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QOfferEmployment(PathMetadata metadata, PathInits inits) { + this(OfferEmployment.class, metadata, inits); + } + + public QOfferEmployment(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.business = inits.isInitialized("business") ? new QBusiness(forProperty("business"), inits.get("business")) : null; + this.contract = inits.isInitialized("contract") ? new QContract(forProperty("contract"), inits.get("contract")) : null; + this.employee = inits.isInitialized("employee") ? new QAccount(forProperty("employee")) : null; + } + +} + diff --git a/src/main/generated/com/example/api/domain/QPossibleBoard.java b/src/main/generated/com/example/api/domain/QPossibleBoard.java new file mode 100644 index 00000000..23743abc --- /dev/null +++ b/src/main/generated/com/example/api/domain/QPossibleBoard.java @@ -0,0 +1,63 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QPossibleBoard is a Querydsl query type for PossibleBoard + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QPossibleBoard extends EntityPathBase { + + private static final long serialVersionUID = -1378693552L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QPossibleBoard possibleBoard = new QPossibleBoard("possibleBoard"); + + public final QBaseEntity _super = new QBaseEntity(this); + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + public final QAccount employee; + + public final DateTimePath endTime = createDateTime("endTime", java.time.LocalDateTime.class); + + public final NumberPath possibleId = createNumber("possibleId", Long.class); + + public final DateTimePath startTime = createDateTime("startTime", java.time.LocalDateTime.class); + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public QPossibleBoard(String variable) { + this(PossibleBoard.class, forVariable(variable), INITS); + } + + public QPossibleBoard(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QPossibleBoard(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QPossibleBoard(PathMetadata metadata, PathInits inits) { + this(PossibleBoard.class, metadata, inits); + } + + public QPossibleBoard(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.employee = inits.isInitialized("employee") ? new QAccount(forProperty("employee")) : null; + } + +} + diff --git a/src/main/generated/com/example/api/domain/QReview.java b/src/main/generated/com/example/api/domain/QReview.java new file mode 100644 index 00000000..30f99fe2 --- /dev/null +++ b/src/main/generated/com/example/api/domain/QReview.java @@ -0,0 +1,72 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QReview is a Querydsl query type for Review + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QReview extends EntityPathBase { + + private static final long serialVersionUID = 731127133L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QReview review = new QReview("review"); + + public final QBaseEntity _super = new QBaseEntity(this); + + public final QContract contract; + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + public final QAccount employee; + + public final QOfferEmployment offerEmployment; + + public final StringPath reviewContent = createString("reviewContent"); + + public final NumberPath reviewId = createNumber("reviewId", Long.class); + + public final NumberPath reviewStarPoint = createNumber("reviewStarPoint", Integer.class); + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public final QBusiness writer; + + public QReview(String variable) { + this(Review.class, forVariable(variable), INITS); + } + + public QReview(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QReview(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QReview(PathMetadata metadata, PathInits inits) { + this(Review.class, metadata, inits); + } + + public QReview(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.contract = inits.isInitialized("contract") ? new QContract(forProperty("contract"), inits.get("contract")) : null; + this.employee = inits.isInitialized("employee") ? new QAccount(forProperty("employee")) : null; + this.offerEmployment = inits.isInitialized("offerEmployment") ? new QOfferEmployment(forProperty("offerEmployment"), inits.get("offerEmployment")) : null; + this.writer = inits.isInitialized("writer") ? new QBusiness(forProperty("writer"), inits.get("writer")) : null; + } + +} + diff --git a/src/main/generated/com/example/api/domain/QReviewReport.java b/src/main/generated/com/example/api/domain/QReviewReport.java new file mode 100644 index 00000000..68d48ced --- /dev/null +++ b/src/main/generated/com/example/api/domain/QReviewReport.java @@ -0,0 +1,53 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QReviewReport is a Querydsl query type for ReviewReport + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QReviewReport extends EntityPathBase { + + private static final long serialVersionUID = 178201841L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QReviewReport reviewReport = new QReviewReport("reviewReport"); + + public final StringPath reason = createString("reason"); + + public final NumberPath reportId = createNumber("reportId", Long.class); + + public final QReview review; + + public QReviewReport(String variable) { + this(ReviewReport.class, forVariable(variable), INITS); + } + + public QReviewReport(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QReviewReport(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QReviewReport(PathMetadata metadata, PathInits inits) { + this(ReviewReport.class, metadata, inits); + } + + public QReviewReport(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.review = inits.isInitialized("review") ? new QReview(forProperty("review"), inits.get("review")) : null; + } + +} + diff --git a/src/main/generated/com/example/api/domain/QScrap.java b/src/main/generated/com/example/api/domain/QScrap.java new file mode 100644 index 00000000..6caf4631 --- /dev/null +++ b/src/main/generated/com/example/api/domain/QScrap.java @@ -0,0 +1,62 @@ +package com.example.api.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QScrap is a Querydsl query type for Scrap + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QScrap extends EntityPathBase { + + private static final long serialVersionUID = -391197396L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QScrap scrap = new QScrap("scrap"); + + public final QBaseEntity _super = new QBaseEntity(this); + + //inherited + public final DateTimePath createdDate = _super.createdDate; + + public final QAccount employee; + + public final QAccount employer; + + public final NumberPath scrapId = createNumber("scrapId", Long.class); + + //inherited + public final DateTimePath updatedDate = _super.updatedDate; + + public QScrap(String variable) { + this(Scrap.class, forVariable(variable), INITS); + } + + public QScrap(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QScrap(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QScrap(PathMetadata metadata, PathInits inits) { + this(Scrap.class, metadata, inits); + } + + public QScrap(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.employee = inits.isInitialized("employee") ? new QAccount(forProperty("employee")) : null; + this.employer = inits.isInitialized("employer") ? new QAccount(forProperty("employer")) : null; + } + +} + diff --git a/src/main/java/com/example/api/ApiApplication.java b/src/main/java/com/example/api/ApiApplication.java index de1cf4b2..633e4efa 100644 --- a/src/main/java/com/example/api/ApiApplication.java +++ b/src/main/java/com/example/api/ApiApplication.java @@ -3,12 +3,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class ApiApplication { - public static void main(String[] args) { - SpringApplication.run(ApiApplication.class, args); } } diff --git a/src/main/java/com/example/api/account/controller/AccountController.java b/src/main/java/com/example/api/account/controller/AccountController.java new file mode 100644 index 00000000..5a48a335 --- /dev/null +++ b/src/main/java/com/example/api/account/controller/AccountController.java @@ -0,0 +1,64 @@ +package com.example.api.account.controller; + +import com.example.api.account.dto.*; +import com.example.api.account.entity.Code; +import com.example.api.account.service.AccountService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequestMapping("/api/v1/account") +@RequiredArgsConstructor +public class AccountController { + private final AccountService signUpService; + private final AccountService accountService; + + @PostMapping("/email/code") + public ResponseEntity sendEmailCode(@Valid @RequestBody final EmailRequest request) { + Code code = signUpService.sendEmail(request); + String successMessage = signUpService.saveCode(code); + return ResponseEntity.ok(successMessage); + } + + @PostMapping("/email/verification") + public ResponseEntity verifyEmail(@Valid @RequestBody final EmailCodeRequest request) { + String successMessage = signUpService.verifyEmail(request); + return ResponseEntity.ok(successMessage); + } + + @PostMapping("/sign-up/employee") + public ResponseEntity signUpEmployee(@Valid @RequestBody final SignUpEmployeeRequest request) { + String successMessage = signUpService.signUpEmployee(request); + return ResponseEntity.status(HttpStatus.CREATED).body(successMessage); + } + + @PostMapping("/sign-up/employer") + public ResponseEntity signUpEmployer(@Valid @RequestBody final SignUpEmployerRequest request) { + String successMessage = signUpService.signUpEmployer(request); + return ResponseEntity.status(HttpStatus.CREATED).body(successMessage); + } + + /** + * @param memberId + * 현재, 로그인된 사용자에 대해서만 계정 삭제 요청이 가능하도록 구현 + * @return + */ + @DeleteMapping("/my") + public ResponseEntity deleteAccount( + @AuthenticationPrincipal final Long memberId + ) { + accountService.deleteAccount(memberId); + return ResponseEntity.ok("delete account"); + } + + @PostMapping("/validation/business-number") + public ResponseEntity verifyBusinessNumber(@Valid @RequestBody final BusinessNumberRequest request) { + String successMessage = signUpService.verifyBusinessNumber(request); + return ResponseEntity.ok(successMessage); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/dto/BusinessNumberRequest.java b/src/main/java/com/example/api/account/dto/BusinessNumberRequest.java new file mode 100644 index 00000000..06744c94 --- /dev/null +++ b/src/main/java/com/example/api/account/dto/BusinessNumberRequest.java @@ -0,0 +1,14 @@ +package com.example.api.account.dto; + +import jakarta.validation.constraints.NotBlank; + +public record BusinessNumberRequest( + @NotBlank + String businessRegistrationNumber, + @NotBlank + String businessName, + @NotBlank + String representationName, + @NotBlank + String businessOpenDate) { +} diff --git a/src/main/java/com/example/api/account/dto/BusinessNumberResponse.java b/src/main/java/com/example/api/account/dto/BusinessNumberResponse.java new file mode 100644 index 00000000..a312d946 --- /dev/null +++ b/src/main/java/com/example/api/account/dto/BusinessNumberResponse.java @@ -0,0 +1,46 @@ +package com.example.api.account.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + + +public record BusinessNumberResponse( + @JsonProperty("request_cnt") int requestCount, + @JsonProperty("valid_cnt") int validCount, + @JsonProperty("status_code") String statusCode, + List data +) { + private record Data( + @JsonProperty("b_no") String businessNumber, + String valid, + @JsonProperty("request_param") RequestParam requestParam, + Status status + ) { + } + private record RequestParam( + @JsonProperty("b_no") String businessNumber, + @JsonProperty("start_dt") String startDate, + @JsonProperty("p_nm") String name, + @JsonProperty("b_nm") String businessName + ) { + } + private record Status( + @JsonProperty("b_no") String businessNumber, + @JsonProperty("b_stt") String businessStatus, + @JsonProperty("b_stt_cd") String businessStatusCode, + @JsonProperty("tax_type") String taxType, + @JsonProperty("tax_type_cd") String taxTypeCode, + @JsonProperty("end_dt") String endDate, + @JsonProperty("utcc_yn") String utccYn, + @JsonProperty("tax_type_change_dt") String taxTypeChangeDate, + @JsonProperty("invoice_apply_dt") String invoiceApplyDate, + @JsonProperty("rbf_tax_type") String rbfTaxType, + @JsonProperty("rbf_tax_type_cd") String rbfTaxTypeCode + ) { + } + + public String getValid(){ + return data.get(0).valid; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/dto/EmailCodeRequest.java b/src/main/java/com/example/api/account/dto/EmailCodeRequest.java new file mode 100644 index 00000000..e0cd62f7 --- /dev/null +++ b/src/main/java/com/example/api/account/dto/EmailCodeRequest.java @@ -0,0 +1,9 @@ +package com.example.api.account.dto; + +import com.example.api.global.config.resolver.ValidEmail; + +public record EmailCodeRequest( + @ValidEmail String email, + String code +) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/dto/EmailRequest.java b/src/main/java/com/example/api/account/dto/EmailRequest.java new file mode 100644 index 00000000..8f5118c0 --- /dev/null +++ b/src/main/java/com/example/api/account/dto/EmailRequest.java @@ -0,0 +1,6 @@ +package com.example.api.account.dto; + +import com.example.api.global.config.resolver.ValidEmail; + +public record EmailRequest(@ValidEmail String email) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/dto/LoginIdRequest.java b/src/main/java/com/example/api/account/dto/LoginIdRequest.java new file mode 100644 index 00000000..e086a7cf --- /dev/null +++ b/src/main/java/com/example/api/account/dto/LoginIdRequest.java @@ -0,0 +1,6 @@ +package com.example.api.account.dto; + +import jakarta.validation.constraints.NotBlank; + +public record LoginIdRequest(@NotBlank String loginId) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/dto/SignUpEmployeeRequest.java b/src/main/java/com/example/api/account/dto/SignUpEmployeeRequest.java new file mode 100644 index 00000000..3ac7a843 --- /dev/null +++ b/src/main/java/com/example/api/account/dto/SignUpEmployeeRequest.java @@ -0,0 +1,29 @@ +package com.example.api.account.dto; + +import com.example.api.account.entity.Nationality; +import com.example.api.account.entity.UserRole; +import com.example.api.global.config.resolver.ValidEmail; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record SignUpEmployeeRequest( + @NotBlank + String loginId, + @NotBlank + String password, + @NotBlank + String name, + @NotBlank + String nickname, + @ValidEmail + String email, + @NotNull + Nationality nationality, + @NotNull + UserRole role, + @NotBlank + String phoneNumber, + @NotNull + Boolean emailReceivable +) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/dto/SignUpEmployerRequest.java b/src/main/java/com/example/api/account/dto/SignUpEmployerRequest.java new file mode 100644 index 00000000..1c9fda8e --- /dev/null +++ b/src/main/java/com/example/api/account/dto/SignUpEmployerRequest.java @@ -0,0 +1,34 @@ +package com.example.api.account.dto; + +import com.example.api.account.entity.Location; +import com.example.api.account.entity.Nationality; +import com.example.api.account.entity.UserRole; +import com.example.api.global.config.resolver.ValidEmail; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record SignUpEmployerRequest( + @NotBlank + String loginId, // 로그인 id + @NotBlank + String password, // 비밀번호 + @ValidEmail + String email, // 이메일 + @NotBlank + String businessRegistrationNumber, // 사업자 번호 + @NotBlank + String businessName, // 회사명 + @NotBlank + String representationName, // 대표명 + @NotBlank + String businessOpenDate, // 개업연월일 + @NotNull + Location location, + @NotNull + Nationality nationality, // 국적 + @NotNull + UserRole role, // 권한 + @NotBlank + String phoneNumber // 휴대폰 번호 +) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/entity/Code.java b/src/main/java/com/example/api/account/entity/Code.java new file mode 100644 index 00000000..8ace57c5 --- /dev/null +++ b/src/main/java/com/example/api/account/entity/Code.java @@ -0,0 +1,26 @@ +package com.example.api.account.entity; + +import jakarta.persistence.Id; +import lombok.Getter; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.Date; + +@Getter +@Document(collection = "code") +public class Code { + @Id + private String id; + private String email; + private String code; + + @Indexed(expireAfterSeconds = 600) + private final Date createdAt; + + public Code(String email, String code) { + this.email = email; + this.code = code; + this.createdAt = new Date(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/entity/CodeGenerator.java b/src/main/java/com/example/api/account/entity/CodeGenerator.java new file mode 100644 index 00000000..4b17243b --- /dev/null +++ b/src/main/java/com/example/api/account/entity/CodeGenerator.java @@ -0,0 +1,31 @@ +package com.example.api.account.entity; + +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; + +@Component +public class CodeGenerator { + @Value("${code.length}") + private int length; + @Value("${code.digit_range}") + private int digitRange; + + public String generateCode() { + try { + Random random = SecureRandom.getInstanceStrong(); + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < length; i++) { + builder.append(random.nextInt(digitRange)); + } + return builder.toString(); + } catch (NoSuchAlgorithmException e) { + throw new BusinessException(ErrorCode.FAIL_GENERATE_CODE); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/entity/Location.java b/src/main/java/com/example/api/account/entity/Location.java new file mode 100644 index 00000000..197dee6f --- /dev/null +++ b/src/main/java/com/example/api/account/entity/Location.java @@ -0,0 +1,45 @@ +package com.example.api.account.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.ToString; + +import java.util.Objects; + +@Entity +@Getter +@ToString +public class Location { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "LOCATION_UNIQUE_ID") + private Long id; + @Column(name = "LOCATION_ZIPCODE") + private String zipcode; + @Column(name = "LOCATION_ADDRESS") + private String address; + @Column(name = "LOCATION_DETAIL_ADDRESS") + private String detailAddress; + + public Location() { + } + + public Location(String zipcode, String address, String detailAddress) { + this.zipcode = zipcode; + this.address = address; + this.detailAddress = detailAddress; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Location location = (Location) o; + return Objects.equals(id, location.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/entity/MailSender.java b/src/main/java/com/example/api/account/entity/MailSender.java new file mode 100644 index 00000000..b7e3e079 --- /dev/null +++ b/src/main/java/com/example/api/account/entity/MailSender.java @@ -0,0 +1,42 @@ +package com.example.api.account.entity; + +import com.example.api.account.dto.EmailRequest; +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MailSender { + private final JavaMailSender mailSender; + private final CodeGenerator codeGenerator; + + @Value("${spring.mail.username}") + private String fromEmail; + + public Code sendEmail(final EmailRequest emailRequest){ + String code = codeGenerator.generateCode(); + + try { + SimpleMailMessage message = createEmail(emailRequest.email(), code); + mailSender.send(message); + + return new Code(emailRequest.email(), code); + } catch (Exception e) { + throw new BusinessException(ErrorCode.FAIL_SEND_EMAIL); + } + } + + private SimpleMailMessage createEmail(final String to, final String code) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo(to); + message.setSubject("단팥의 인증 이메일입니다."); + message.setText(String.format("단팥 인증코드 %s 입니다.", code)); + message.setFrom(fromEmail); + return message; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/entity/Nationality.java b/src/main/java/com/example/api/account/entity/Nationality.java new file mode 100644 index 00000000..58ac54cd --- /dev/null +++ b/src/main/java/com/example/api/account/entity/Nationality.java @@ -0,0 +1,27 @@ +package com.example.api.account.entity; + +public enum Nationality { + KOREAN(0, "내국인"), + FOREIGN(1, "외국인"); + + private final Integer code; + private final String description; + + Nationality(final Integer code, final String description) { + this.code = code; + this.description = description; + } + + public static Nationality of(final Integer code) { + for (Nationality nationality : values()) { + if (nationality.getCode().equals(code)) { + return nationality; + } + } + return null; + } + + public Integer getCode() { + return code; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/entity/UserRole.java b/src/main/java/com/example/api/account/entity/UserRole.java new file mode 100644 index 00000000..9848f010 --- /dev/null +++ b/src/main/java/com/example/api/account/entity/UserRole.java @@ -0,0 +1,50 @@ +package com.example.api.account.entity; + +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import com.fasterxml.jackson.annotation.JsonCreator; +import org.springframework.security.core.GrantedAuthority; + +public enum UserRole implements GrantedAuthority { + EMPLOYEE(0, "알바생"), + EMPLOYER(1, "사장"); + + private final Integer code; + private final String description; + + UserRole(final Integer code, final String description) { + this.code = code; + this.description = description; + } + + public static UserRole of(final Integer code) { + for (UserRole role : values()) { + if (role.getCode().equals(code)) { + return role; + } + } + return null; + } + + @JsonCreator + public static UserRole from(String value) { + for (UserRole role : values()) { + if (role.name().equalsIgnoreCase(value)) { + return role; + } + } + throw new BusinessException(ErrorCode.INCORRECT_DATA); + } + + public Integer getCode() { + return code; + } + + public String getDescription() { + return description; + } + + public String getAuthority() { + return this.name(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/repository/AccountRepository.java b/src/main/java/com/example/api/account/repository/AccountRepository.java new file mode 100644 index 00000000..363aa6f5 --- /dev/null +++ b/src/main/java/com/example/api/account/repository/AccountRepository.java @@ -0,0 +1,38 @@ +package com.example.api.account.repository; + +import com.example.api.domain.Account; +import com.example.api.offeremployment.dto.StarPointAndWorkCountRequest; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.repository.query.Param; +import org.springframework.security.core.parameters.P; + +import java.util.Optional; + +public interface AccountRepository extends JpaRepository { + boolean existsByLoginId(String loginId); + + boolean existsByEmail(String email); + + @EntityGraph(attributePaths = "roles") + Optional findByEmail(String email); + + Optional findUserByLoginId(String loginId); + + @Query("SELECT a.profileImage FROM Account a WHERE a.accountId = :accountId") + Optional findProfileImageByAccountId(@Param("accountId") Long accountId); + + @Modifying + @Query("update Account a set a.profileImage = :profileImage where a.accountId = :accountId") + void updateProfileImageByAccountId(@Param("profileImage") String profileImage, @Param("accountId") Long accountId); + + @Modifying + @Query("update Account a " + + "set a.starPoint = ((a.starPoint * a.workCount) + :starPoint) / (a.workCount+1), " + + "a.workCount = a.workCount + 1 " + + "where a.accountId in " + + "(select oe.employee.accountId From OfferEmployment oe where oe.suggestId = :suggestId)") + void updateWorkCountAndStarPointBySuggestId(@Param("suggestId") Long suggestId, @Param("starPoint") Integer newStarPoint); +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/repository/CodeRepository.java b/src/main/java/com/example/api/account/repository/CodeRepository.java new file mode 100644 index 00000000..a73ff693 --- /dev/null +++ b/src/main/java/com/example/api/account/repository/CodeRepository.java @@ -0,0 +1,12 @@ +package com.example.api.account.repository; + +import com.example.api.account.entity.Code; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface CodeRepository extends MongoRepository { + Optional findFirstByEmailOrderByCreatedAtDesc(String email); +} \ No newline at end of file diff --git a/src/main/java/com/example/api/account/repository/LocationRepository.java b/src/main/java/com/example/api/account/repository/LocationRepository.java new file mode 100644 index 00000000..80b9b8c3 --- /dev/null +++ b/src/main/java/com/example/api/account/repository/LocationRepository.java @@ -0,0 +1,9 @@ +package com.example.api.account.repository; + +import com.example.api.account.entity.Location; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface LocationRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/api/account/service/AccountService.java b/src/main/java/com/example/api/account/service/AccountService.java new file mode 100644 index 00000000..d0386ba4 --- /dev/null +++ b/src/main/java/com/example/api/account/service/AccountService.java @@ -0,0 +1,207 @@ +package com.example.api.account.service; + +import com.example.api.account.dto.*; +import com.example.api.account.entity.Code; +import com.example.api.account.entity.Location; +import com.example.api.account.entity.UserRole; +import com.example.api.account.repository.AccountRepository; +import com.example.api.account.repository.CodeRepository; +import com.example.api.account.entity.MailSender; +import com.example.api.account.repository.LocationRepository; +import com.example.api.business.BusinessRepository; +import com.example.api.domain.Account; +import com.example.api.domain.Business; +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import com.example.api.global.properties.VendorProperties; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.*; + +@Service +@RequiredArgsConstructor +@Slf4j +public class AccountService { + private final AccountRepository accountRepository; + private final CodeRepository codeRepository; + private final PasswordEncoder passwordEncoder; + private final MailSender mailSender; + private final BusinessRepository businessRepository; + private final RestTemplate restTemplate; + private final VendorProperties vendorProperties; + private final LocationRepository locationRepository; + + public Code sendEmail(@Validated final EmailRequest request) throws BusinessException { + // 이미 가입된 이메일인지 검증 + validateDuplicateEmail(request); + return mailSender.sendEmail(request); + } + + @Transactional + public String saveCode(@Validated final Code code){ + try { + codeRepository.save(code); + return "이메일 전송을 완료하였습니다."; + } catch (Exception e){ + throw new BusinessException(ErrorCode.FAIL_SAVE_CODE); + } + } + + @Transactional + public String verifyEmail(@Validated final EmailCodeRequest request) { + Optional findCode = codeRepository.findFirstByEmailOrderByCreatedAtDesc(request.email()); + + return findCode.map(code -> { + if (code.getCode().equals(request.code())) { + return "유효한 이메일입니다."; + } else { + throw new BusinessException(ErrorCode.INCORRECT_CODE); + } + }).orElseThrow(() -> new BusinessException(ErrorCode.EXPIRATION_DATE_END)); + } + + @Transactional + public String signUpEmployee(@Validated final SignUpEmployeeRequest request) { + // 중복 로그인 ID 확인 + validateDuplicateLoginId(new LoginIdRequest(request.loginId())); + // 계정 저장 + saveEmployeeAccount(request); + return "회원가입이 완료되었습니다"; + } + + @Transactional + public String signUpEmployer(@Valid final SignUpEmployerRequest request) { + // 중복 로그인 ID 확인 (사장) + validateDuplicateLoginId(new LoginIdRequest(request.loginId())); + // 계정 저장 + saveEmployerAccount(request); + return "회원가입이 완료되었습니다"; + } + + @Transactional(readOnly = true) + public Account loadAccount(final Long requestMemberId) { + return accountRepository.findById(requestMemberId) + .orElseThrow(() -> new BusinessException(ErrorCode.ACCOUNT_NOT_FOUND_EXCEPTION)); + } + + @Transactional + public void deleteAccount(final Long requestMemberId) { + final Account account = accountRepository.findById(requestMemberId) + .orElseThrow(() -> new BusinessException(ErrorCode.ACCOUNT_NOT_FOUND_EXCEPTION)); + account.setDeleted(true); + } + private Account saveEmployeeAccount(final SignUpEmployeeRequest request) { + Collection roles = List.of(request.role()); + Account account = new Account( + request.loginId(), + passwordEncoder.encode(request.password()), + request.name(), + request.nickname(), + request.email(), + request.phoneNumber(), + request.nationality(), + roles, + request.emailReceivable() + ); + return accountRepository.save(account); + } + + private void saveEmployerAccount(final SignUpEmployerRequest request) { + Collection roles = List.of(request.role()); + Account account = new Account( + request.loginId(), + passwordEncoder.encode(request.password()), + request.email(), + request.phoneNumber(), + request.nationality(), + roles + ); + Account savedUser = accountRepository.save(account); + + Location savedLocation = locationRepository.save(request.location()); + Business business = new Business( + savedUser, + request.businessRegistrationNumber(), + request.businessName(), + request.representationName(), + request.businessOpenDate(), + savedLocation + ); + businessRepository.save(business); + accountRepository.save(account); + } + + private void validateDuplicateLoginId(final LoginIdRequest loginIdRequest) { + if (accountRepository.existsByLoginId(loginIdRequest.loginId())) { + throw new BusinessException(ErrorCode.DUPLICATE_LOGIN_ID); + } + } + + private void validateDuplicateEmail(final EmailRequest emailRequest) { + if (accountRepository.existsByEmail(emailRequest.email())) { + throw new BusinessException(ErrorCode.DUPLICATE_EMAIL); + } + } + + @Transactional + public String verifyBusinessNumber(@Validated final BusinessNumberRequest request){ + URI uri = createUrl(); + HttpEntity> requestEntity = getBusinessValidateApiRequestEntity(request); + + ResponseEntity response = restTemplate.exchange( + uri, + HttpMethod.POST, + requestEntity, + BusinessNumberResponse.class + ); + + log.info("response.getBody()={}", response.getBody()); + + String valid = Optional.ofNullable(response.getBody()) + .map(BusinessNumberResponse::getValid) + .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_BUSINESS_NUMBER)); + + if (!"01".equals(valid)) { + throw new BusinessException(ErrorCode.INVALID_BUSINESS_NUMBER); + } + + return "유효한 사업자 등록 정보입니다."; + } + + @NotNull + private URI createUrl() { + try { + String encodedServiceKey = URLEncoder.encode(vendorProperties.getServiceKey(), StandardCharsets.UTF_8); + String url = vendorProperties.getBaseUrl() + "?serviceKey=" + encodedServiceKey; + return new URI(url); + } catch (Exception e) { + log.error(e.getMessage()); + // 프론트 서버 배포 후 수정, 회원가입 end point로 변경 + return URI.create("http://localhost:3000/"); + } + } + + @NotNull + private HttpEntity> getBusinessValidateApiRequestEntity(BusinessNumberRequest request) { + Map business = new HashMap<>(); + business.put("b_no", request.businessRegistrationNumber()); + business.put("start_dt", request.businessOpenDate()); + business.put("p_nm", request.representationName()); + business.put("b_nm", request.businessName()); + return new HttpEntity<>(Collections.singletonMap("businesses", Collections.singletonList(business))); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/announcement/AnnouncementRepository.java b/src/main/java/com/example/api/announcement/AnnouncementRepository.java new file mode 100644 index 00000000..4d899a31 --- /dev/null +++ b/src/main/java/com/example/api/announcement/AnnouncementRepository.java @@ -0,0 +1,12 @@ +package com.example.api.announcement; + +import com.example.api.domain.Announcement; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface AnnouncementRepository extends JpaRepository { + List findByAnnouncementTitleContaining(final String keyword); +} diff --git a/src/main/java/com/example/api/announcement/AnnouncementService.java b/src/main/java/com/example/api/announcement/AnnouncementService.java new file mode 100644 index 00000000..3529239f --- /dev/null +++ b/src/main/java/com/example/api/announcement/AnnouncementService.java @@ -0,0 +1,92 @@ +package com.example.api.announcement; + +import com.example.api.announcement.dto.AnnouncementCommand; +import com.example.api.announcement.dto.AnnouncementResponse; +import com.example.api.domain.Announcement; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class AnnouncementService { + private final AnnouncementRepository announcementRepository; + + @Transactional + public AnnouncementResponse createAnnouncement(@Validated final AnnouncementCommand command) { + Announcement announcement = new Announcement( + null, + command.announcementTitle(), + command.announcementType(), + command.announcementContent(), + 0 + ); + Announcement savedAnnouncement = announcementRepository.save(announcement); + return new AnnouncementResponse(savedAnnouncement); + } + + @Transactional + public List getAllAnnouncements() { + final List announcements = announcementRepository.findAll(); + return announcements.stream() + .map(AnnouncementResponse::new) + .collect(Collectors.toList()); + } + + @Transactional + public AnnouncementResponse getAnnouncement(@Validated final Long announcementId) { + final Announcement announcement = findAnnouncementById(announcementId); + return new AnnouncementResponse(announcement); + } + + @Transactional + public AnnouncementResponse updateAnnouncement( + @Validated final Long announcementId, + @Validated final AnnouncementCommand command + ) { + Announcement announcement = findAnnouncementById(announcementId); + announcement = new Announcement( + announcement.getAnnouncementId(), + command.announcementTitle(), + command.announcementType(), + command.announcementContent(), + announcement.getViewCount() + ); + Announcement updatedAnnouncement = announcementRepository.save(announcement); + return new AnnouncementResponse(updatedAnnouncement); + } + + @Transactional + public void deleteAnnouncement( + @Validated final Long memberId, + @Validated final Long announcementId + ) { + final Announcement announcement = findAnnouncementById(announcementId); + announcementRepository.delete(announcement); + } + + @Transactional + public List searchAnnouncements( + @Validated final String keyword + ) { + final List announcements = announcementRepository.findByAnnouncementTitleContaining(keyword); + return announcements.stream() + .map(AnnouncementResponse::new) + .collect(Collectors.toList()); + } + + private Announcement findAnnouncementById(@Validated final Long announcementId) { + return announcementRepository.findById(announcementId) + .orElseThrow(() -> new RuntimeException(getErrorMessage("announcement.not.found"))); + } + + private String getErrorMessage(final String key) { + return key; + } +} + + + diff --git a/src/main/java/com/example/api/announcement/controller/AnnouncementController.java b/src/main/java/com/example/api/announcement/controller/AnnouncementController.java new file mode 100644 index 00000000..8d7762a0 --- /dev/null +++ b/src/main/java/com/example/api/announcement/controller/AnnouncementController.java @@ -0,0 +1,73 @@ +package com.example.api.announcement.controller; + +import com.example.api.announcement.AnnouncementService; +import com.example.api.announcement.dto.AnnouncementCommand; +import com.example.api.announcement.dto.AnnouncementRequest; +import com.example.api.announcement.dto.AnnouncementResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/support/announcements") +public class AnnouncementController { + private final AnnouncementService announcementService; + + @PostMapping + public ResponseEntity createAnnouncement( + @AuthenticationPrincipal final Long memberId, + @Valid @RequestBody final AnnouncementRequest request + ) { + final AnnouncementCommand command = request.toCommand(memberId); + final AnnouncementResponse response = announcementService.createAnnouncement(command); + return ResponseEntity.ok(response); + } + + @GetMapping + public ResponseEntity> getAnnouncements() { + final List responses = announcementService.getAllAnnouncements(); + return ResponseEntity.ok(responses); + } + + @GetMapping("/{announcementId}") + public ResponseEntity getAnnouncement( + @PathVariable(required = true) final Long announcementId + ) { + final AnnouncementResponse response = announcementService.getAnnouncement(announcementId); + return ResponseEntity.ok(response); + } + + @PutMapping("/{announcementId}") + public ResponseEntity updateAnnouncement( + @AuthenticationPrincipal final Long memberId, + @PathVariable(required = true) final Long announcementId, + @Valid @RequestBody final AnnouncementRequest request + ) { + final AnnouncementCommand command = request.toCommand(memberId); + final AnnouncementResponse response = announcementService.updateAnnouncement(announcementId, command); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{announcementId}") + public ResponseEntity deleteAnnouncement( + @AuthenticationPrincipal final Long memberId, + @PathVariable final Long announcementId + ) { + announcementService.deleteAnnouncement(memberId, announcementId); + return ResponseEntity.ok().build(); + } + + + @GetMapping("/search") + public ResponseEntity> searchAnnouncements( + @RequestParam(required = true) final String keyword + ) { + final List responses = announcementService.searchAnnouncements(keyword); + return ResponseEntity.ok(responses); + } +} diff --git a/src/main/java/com/example/api/announcement/dto/AnnouncementCommand.java b/src/main/java/com/example/api/announcement/dto/AnnouncementCommand.java new file mode 100644 index 00000000..dffcf784 --- /dev/null +++ b/src/main/java/com/example/api/announcement/dto/AnnouncementCommand.java @@ -0,0 +1,12 @@ +package com.example.api.announcement.dto; + +import lombok.NonNull; + +public record AnnouncementCommand( + @NonNull + Long memberId, + @NonNull + String announcementTitle, + String announcementType, + String announcementContent +) {} diff --git a/src/main/java/com/example/api/announcement/dto/AnnouncementRequest.java b/src/main/java/com/example/api/announcement/dto/AnnouncementRequest.java new file mode 100644 index 00000000..4a02da4a --- /dev/null +++ b/src/main/java/com/example/api/announcement/dto/AnnouncementRequest.java @@ -0,0 +1,21 @@ +package com.example.api.announcement.dto; + +import lombok.NonNull; + +public record AnnouncementRequest( + @NonNull + String announcementTitle, + String announcementType, + String announcementContent +) { + public AnnouncementCommand toCommand(Long memberId) { + return new AnnouncementCommand( + memberId, + this.announcementTitle, + this.announcementType, + this.announcementContent + ); + } +} + + diff --git a/src/main/java/com/example/api/announcement/dto/AnnouncementResponse.java b/src/main/java/com/example/api/announcement/dto/AnnouncementResponse.java new file mode 100644 index 00000000..0ef7a250 --- /dev/null +++ b/src/main/java/com/example/api/announcement/dto/AnnouncementResponse.java @@ -0,0 +1,22 @@ +package com.example.api.announcement.dto; + +import com.example.api.domain.Announcement; + +public record AnnouncementResponse( + Long announcementId, + String announcementTitle, + String announcementType, + String announcementContent, + int viewCount +) { + public AnnouncementResponse(Announcement announcement) { + this( + announcement.getAnnouncementId(), + announcement.getAnnouncementTitle(), + announcement.getAnnouncementType(), + announcement.getAnnouncementContent(), + announcement.getViewCount() + ); + } +} + diff --git a/src/main/java/com/example/api/auth/controller/AuthController.java b/src/main/java/com/example/api/auth/controller/AuthController.java new file mode 100644 index 00000000..f130a770 --- /dev/null +++ b/src/main/java/com/example/api/auth/controller/AuthController.java @@ -0,0 +1,40 @@ +package com.example.api.auth.controller; + +import com.example.api.auth.dto.LoginRequest; +import com.example.api.auth.dto.LoginSuccessResponse; +import com.example.api.auth.dto.LoginUserRequest; +import com.example.api.auth.dto.RefreshTokenRequest; +import com.example.api.auth.service.AuthService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/auth") +@RequiredArgsConstructor +public class AuthController { + private final AuthService authService; + private final AuthenticationManager authenticationManager; + + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody final LoginRequest loginRequest) { + LoginSuccessResponse loginSuccessResponse = authService.login(loginRequest); + return ResponseEntity.ok(loginSuccessResponse); + } + + @PostMapping("/refresh") + public ResponseEntity refresh(@Valid @RequestBody final RefreshTokenRequest refreshTokenRequest) { + LoginSuccessResponse loginSuccessResponse = authService.refreshAuthToken(refreshTokenRequest); + return ResponseEntity.ok(loginSuccessResponse); + } + + @PostMapping("/logout") + public ResponseEntity logout(@AuthenticationPrincipal final Object principal) { + Long userId = Long.parseLong(principal.toString()); + LoginSuccessResponse loginSuccessResponse = authService.logout(new LoginUserRequest(userId)); + return ResponseEntity.ok(loginSuccessResponse); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/auth/dto/AuthTokenRequest.java b/src/main/java/com/example/api/auth/dto/AuthTokenRequest.java new file mode 100644 index 00000000..48ca4b32 --- /dev/null +++ b/src/main/java/com/example/api/auth/dto/AuthTokenRequest.java @@ -0,0 +1,7 @@ +package com.example.api.auth.dto; + +public record AuthTokenRequest( + String accessToken, + String refreshToken +) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/auth/dto/LoginRequest.java b/src/main/java/com/example/api/auth/dto/LoginRequest.java new file mode 100644 index 00000000..b997c08d --- /dev/null +++ b/src/main/java/com/example/api/auth/dto/LoginRequest.java @@ -0,0 +1,9 @@ +package com.example.api.auth.dto; + +import jakarta.validation.constraints.NotBlank; + +public record LoginRequest( + @NotBlank String loginId, + @NotBlank String password +) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/auth/dto/LoginSuccessResponse.java b/src/main/java/com/example/api/auth/dto/LoginSuccessResponse.java new file mode 100644 index 00000000..75aaa111 --- /dev/null +++ b/src/main/java/com/example/api/auth/dto/LoginSuccessResponse.java @@ -0,0 +1,9 @@ +package com.example.api.auth.dto; + +public record LoginSuccessResponse( + String accessToken, + String refreshToken, + String userId, + String userRole +) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/auth/dto/LoginUserRequest.java b/src/main/java/com/example/api/auth/dto/LoginUserRequest.java new file mode 100644 index 00000000..f0abb90a --- /dev/null +++ b/src/main/java/com/example/api/auth/dto/LoginUserRequest.java @@ -0,0 +1,4 @@ +package com.example.api.auth.dto; + +public record LoginUserRequest(Long userId) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/auth/dto/RefreshTokenRequest.java b/src/main/java/com/example/api/auth/dto/RefreshTokenRequest.java new file mode 100644 index 00000000..dc01eb0d --- /dev/null +++ b/src/main/java/com/example/api/auth/dto/RefreshTokenRequest.java @@ -0,0 +1,6 @@ +package com.example.api.auth.dto; + +import jakarta.validation.constraints.NotBlank; + +public record RefreshTokenRequest(@NotBlank String refreshToken) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/auth/dto/UserDetailRequest.java b/src/main/java/com/example/api/auth/dto/UserDetailRequest.java new file mode 100644 index 00000000..e7522e9b --- /dev/null +++ b/src/main/java/com/example/api/auth/dto/UserDetailRequest.java @@ -0,0 +1,12 @@ +package com.example.api.auth.dto; + +import com.example.api.account.entity.UserRole; +import jakarta.validation.constraints.NotNull; + +import java.util.Collection; + +public record UserDetailRequest( + @NotNull Long userId, + @NotNull Collection authorities +) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/auth/entitiy/CustomUserDetailService.java b/src/main/java/com/example/api/auth/entitiy/CustomUserDetailService.java new file mode 100644 index 00000000..2b7a51ef --- /dev/null +++ b/src/main/java/com/example/api/auth/entitiy/CustomUserDetailService.java @@ -0,0 +1,33 @@ +package com.example.api.auth.entitiy; + +import com.example.api.account.repository.AccountRepository; +import com.example.api.domain.Account; +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; + +@Component +@RequiredArgsConstructor +public class CustomUserDetailService { + private final AccountRepository accountRepository; + + @Transactional(readOnly = true) + public CustomUserDetails loadUserByUserId(final Long userId) { + Account user = accountRepository.findById(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.NULL_USER)); + + if(user.isDeleted()) + throw new BusinessException(ErrorCode.DELETED_USER); + + Collection authorities = user.getRoles().stream() + .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) + .toList(); + + return new CustomUserDetails(user.getAccountId(), user.getName(), user.getEmail(), authorities); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/auth/entitiy/CustomUserDetails.java b/src/main/java/com/example/api/auth/entitiy/CustomUserDetails.java new file mode 100644 index 00000000..7eedc0e7 --- /dev/null +++ b/src/main/java/com/example/api/auth/entitiy/CustomUserDetails.java @@ -0,0 +1,48 @@ +package com.example.api.auth.entitiy; + +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.Map; + +@Getter +public class CustomUserDetails implements UserDetails, OAuth2User { + private final Long userId; + private final String name; + private final String email; + private final Collection authorities; + + public CustomUserDetails(final Long userId, final String name, final String email, final Collection authorities) { + this.userId = userId; + this.name = name; + this.email = email; + this.authorities = authorities; + } + + @Override + public Map getAttributes(){ + return Map.of( + "userId", userId, + "name", name, + "email", email + ); + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getUsername() { + return this.email; + } + + @Override + public String getPassword() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/auth/entitiy/JwtAuthenticationProvider.java b/src/main/java/com/example/api/auth/entitiy/JwtAuthenticationProvider.java new file mode 100644 index 00000000..f51080fe --- /dev/null +++ b/src/main/java/com/example/api/auth/entitiy/JwtAuthenticationProvider.java @@ -0,0 +1,40 @@ +package com.example.api.auth.entitiy; + +import com.example.api.auth.service.JwtTokenProvider; +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationProvider implements AuthenticationProvider { + private final JwtTokenProvider jwtTokenProvider; + private final CustomUserDetailService customUserDetailService; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (!(authentication instanceof JwtAuthenticationToken)) { + return null; + } + String accessToken = authentication.getCredentials().toString(); + + Claims claims = jwtTokenProvider.getClaimsByToken(accessToken); + if (claims.get("auth") == null) { + throw new BusinessException(ErrorCode.TOKEN_MISSING_AUTHORITY); + } + + Long userId = claims.get("userId", Long.class); + + CustomUserDetails customUserDetails = customUserDetailService.loadUserByUserId(userId); + return new JwtAuthenticationToken(userId, accessToken, customUserDetails.getAuthorities()); + } + + public boolean supports(final Class authentication) { + return JwtAuthenticationToken.class.isAssignableFrom(authentication); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/auth/entitiy/JwtAuthenticationToken.java b/src/main/java/com/example/api/auth/entitiy/JwtAuthenticationToken.java new file mode 100644 index 00000000..f867ceab --- /dev/null +++ b/src/main/java/com/example/api/auth/entitiy/JwtAuthenticationToken.java @@ -0,0 +1,49 @@ +package com.example.api.auth.entitiy; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class JwtAuthenticationToken extends AbstractAuthenticationToken { + private final Long userId; + private String token; + + public JwtAuthenticationToken(String token) { + super(null); + this.userId = null; + this.token = token; + setAuthenticated(false); + } + + public JwtAuthenticationToken(Long userId, String token, Collection authorities) { + super(authorities); + this.userId = userId; + this.token = token; + setAuthenticated(false); + } + + @Override + public Object getCredentials() { + return this.token; + } + + @Override + public Object getPrincipal() { + return this.userId; + } + + @Override + public void setAuthenticated(boolean authenticated) throws IllegalArgumentException { + if (authenticated) { + throw new IllegalArgumentException("옳지 않은 과정을 통해 인증되었습니다."); + } + super.setAuthenticated(false); + } + + @Override + public void eraseCredentials() { + super.eraseCredentials(); + this.token = null; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/auth/entitiy/RefreshToken.java b/src/main/java/com/example/api/auth/entitiy/RefreshToken.java new file mode 100644 index 00000000..d461e29c --- /dev/null +++ b/src/main/java/com/example/api/auth/entitiy/RefreshToken.java @@ -0,0 +1,50 @@ +package com.example.api.auth.entitiy; + +import com.example.api.domain.Account; +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity +public class RefreshToken { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "tokenId") + private Long id; + + @Column + private String refreshToken; + + @Column + private boolean isExpired = false; + + @Column + private LocalDateTime recentLogin = LocalDateTime.now(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ACCOUNT_UNIQUE_ID", nullable = true, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + private Account user; + + protected RefreshToken() { + } + + public RefreshToken(Account user) { + this.user = user; + } + + public Long getId() { + return id; + } + + public void putRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public void expire() { + isExpired = true; + } + + public boolean isExpired() { + return isExpired; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/auth/repository/TokenRepository.java b/src/main/java/com/example/api/auth/repository/TokenRepository.java new file mode 100644 index 00000000..61379fd1 --- /dev/null +++ b/src/main/java/com/example/api/auth/repository/TokenRepository.java @@ -0,0 +1,15 @@ +package com.example.api.auth.repository; + +import com.example.api.auth.entitiy.RefreshToken; +import com.example.api.domain.Account; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface TokenRepository extends JpaRepository { + Optional findByUser(Account user); + + void deleteAllByUser(Account user); +} \ No newline at end of file diff --git a/src/main/java/com/example/api/auth/service/AuthService.java b/src/main/java/com/example/api/auth/service/AuthService.java new file mode 100644 index 00000000..35bf5a05 --- /dev/null +++ b/src/main/java/com/example/api/auth/service/AuthService.java @@ -0,0 +1,89 @@ +package com.example.api.auth.service; + +import com.example.api.account.repository.AccountRepository; +import com.example.api.auth.entitiy.RefreshToken; +import com.example.api.auth.dto.*; +import com.example.api.auth.repository.TokenRepository; +import com.example.api.domain.Account; +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +@Service +@RequiredArgsConstructor +public class AuthService { + private final AccountRepository accountRepository; + private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; + private final TokenRepository tokenRepository; + + @Transactional + public LoginSuccessResponse login(@Validated final LoginRequest request) { + final Account user = getUserByLoginId(request.loginId()); + checkPassword(request, user); + return generateAuthToken(user); + } + + private Account getUserByLoginId(final String loginId) { + final Account user = accountRepository.findUserByLoginId(loginId) + .orElseThrow(() -> new BusinessException(ErrorCode.NULL_USER)); + + if(user.isDeleted()) + throw new BusinessException(ErrorCode.DELETED_USER); + + return user; + } + + private void checkPassword(final LoginRequest request, final Account user) { + if(!passwordEncoder.matches(request.password(), user.getPassword())) { + throw new BusinessException(ErrorCode.INCORRECT_PASSWORD); + } + } + + private LoginSuccessResponse generateAuthToken(final Account user) { + String accessToken = jwtTokenProvider.generateAccessToken(new UserDetailRequest(user.getAccountId(), user.getRoles())); + String refreshToken = generateRefreshToken(user); + String role = user.getRoles().stream().findFirst().get().getAuthority(); // 회원가입 시에 무조건 역할이 들어가기에 바로 get으로 꺼냄 + return new LoginSuccessResponse(accessToken,refreshToken, user.getAccountId().toString(), role); + } + + private String generateRefreshToken(final Account user) { + RefreshToken token = new RefreshToken(user); + + if(tokenRepository.findByUser(user).isPresent()) { + tokenRepository.deleteAllByUser(user); + } + + tokenRepository.save(token); + String refreshToken = jwtTokenProvider.generateRefreshToken(new UserDetailRequest(user.getAccountId(), user.getRoles()), token.getId()); + + token.putRefreshToken(refreshToken); + return refreshToken; + } + + @Transactional + public LoginSuccessResponse refreshAuthToken(@Validated final RefreshTokenRequest request){ + if(!jwtTokenProvider.isNotExpiredToken(request.refreshToken())){ + throw new BusinessException(ErrorCode.EXPIRED_REFRESH_TOKEN); + } + + Long userId = jwtTokenProvider.getUserIdFromToken(request.refreshToken()); + Account user = getUserById(userId); + return generateAuthToken(user); + } + + @Transactional + public LoginSuccessResponse logout(@Validated final LoginUserRequest loginUserRequest) { + Account user = getUserById(loginUserRequest.userId()); + tokenRepository.deleteAllByUser(user); + return new LoginSuccessResponse(null, null, null, null); + } + + private Account getUserById(final Long userId) { + return accountRepository.findById(userId).orElseThrow(() -> new BusinessException(ErrorCode.NULL_USER)); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/auth/service/JwtTokenProvider.java b/src/main/java/com/example/api/auth/service/JwtTokenProvider.java new file mode 100644 index 00000000..9da5979f --- /dev/null +++ b/src/main/java/com/example/api/auth/service/JwtTokenProvider.java @@ -0,0 +1,128 @@ +package com.example.api.auth.service; + +import com.example.api.auth.dto.UserDetailRequest; +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import com.example.api.global.properties.JwtProperties; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + private final JwtProperties jwtProperties; + + // AccessToken 생성 + public String generateAccessToken(final UserDetailRequest user) { + Claims claims = getClaimsFrom(user); + return getTokenFrom(claims, jwtProperties.getAccessTokenValidTime() * 1000); + } + + // AccessToken용 Claim 생성 + private Claims getClaimsFrom(final UserDetailRequest user) { + Claims claims = Jwts.claims(); + claims.put("userId", user.userId()); + claims.put("auth", user.authorities()); + return claims; + } + + // RefrshToken 생성 + public String generateRefreshToken(final UserDetailRequest user, final Long tokenId) { + Claims claims = getClaimsFrom(user, tokenId); + return getTokenFrom(claims, jwtProperties.getRefreshTokenValidTime() * 1000); + } + + // RefreshToken용 Claim 생성 + private Claims getClaimsFrom(final UserDetailRequest user, final Long tokenId) { + Claims claims = Jwts.claims(); + claims.put("userId", user.userId()); + claims.put("tokenId", tokenId); + claims.put("auth", user.authorities()); + return claims; + } + + // claim 정보로 Token 얻기 + private String getTokenFrom(final Claims claims, final long validTime) { + Date now = new Date(); + return Jwts.builder() + .setHeaderParam("type", "JWT") + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + validTime)) + .signWith( + Keys.hmacShaKeyFor(jwtProperties.getBytesSecretKey()), + SignatureAlgorithm.HS256 + ) + .compact(); + } + + // Token에서 유저 인증 정보 얻기 + public Long getUserIdFromToken(final String token) { + try { + Claims claims = Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(jwtProperties.getBytesSecretKey())) + .build() + .parseClaimsJws(token) + .getBody(); + return claims.get("userId", Long.class); + } catch (ExpiredJwtException e) { + throw new BusinessException(ErrorCode.EXPIRED_ACCESS_TOKEN); + } catch (Exception e) { + throw new BusinessException(ErrorCode.INVALID_TOKEN); + } + } + + // AccessToken 값만 남도록 접두사 삭제 + public String extractAccessToken(final HttpServletRequest request) { + String token = request.getHeader("Authorization"); + if (token != null && token.startsWith("Bearer ")) { + return token.substring(7); + } + return token; + } + + // 만료된 토큰인지 확인 + public boolean isNotExpiredToken(final String token) { + try { + return !Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(jwtProperties.getBytesSecretKey())) + .build() + .parseClaimsJws(token) + .getBody() + .getExpiration().before(new Date()); + } catch (ExpiredJwtException e) { + return false; + } + } + + // 토큰으로부터 토큰 ID 얻기 + public Long getTokenIdFromToken(final String refreshToken) { + try { + Claims claims = Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(jwtProperties.getBytesSecretKey())) + .build() + .parseClaimsJws(refreshToken) + .getBody(); + + return Long.parseLong(String.valueOf(claims.get("tokenId"))); + } catch (ExpiredJwtException e) { + throw new BusinessException(ErrorCode.EXPIRED_ACCESS_TOKEN); + } catch (Exception e) { + throw new BusinessException(ErrorCode.INVALID_TOKEN); + } + } + + // 토큰으로부터 Claims 얻기 + public Claims getClaimsByToken(final String accessToken) { + return Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(jwtProperties.getBytesSecretKey())) + .build() + .parseClaimsJws(accessToken) + .getBody(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/aws/controller/S3Controller.java b/src/main/java/com/example/api/aws/controller/S3Controller.java new file mode 100644 index 00000000..4bd0f652 --- /dev/null +++ b/src/main/java/com/example/api/aws/controller/S3Controller.java @@ -0,0 +1,23 @@ +package com.example.api.aws.controller; + +import com.example.api.aws.dto.UploadProfileRequest; +import com.example.api.aws.service.S3Service; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@AllArgsConstructor +@RestController +@RequestMapping("/api/v1") +public class S3Controller { + private final S3Service s3Service; + + @PostMapping(value = "/upload/profile", consumes = "multipart/form-data") + public ResponseEntity upload(@RequestParam("file") final MultipartFile file) { + UploadProfileRequest request = new UploadProfileRequest(1L, file); + return new ResponseEntity<>(s3Service.upload(request).path(), HttpStatus.OK); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/aws/dto/OldKeyRequest.java b/src/main/java/com/example/api/aws/dto/OldKeyRequest.java new file mode 100644 index 00000000..2f5ae4e6 --- /dev/null +++ b/src/main/java/com/example/api/aws/dto/OldKeyRequest.java @@ -0,0 +1,6 @@ +package com.example.api.aws.dto; + +import jakarta.validation.constraints.NotBlank; + +public record OldKeyRequest(@NotBlank String oldKey) { +} diff --git a/src/main/java/com/example/api/aws/dto/S3UploadRequest.java b/src/main/java/com/example/api/aws/dto/S3UploadRequest.java new file mode 100644 index 00000000..63083695 --- /dev/null +++ b/src/main/java/com/example/api/aws/dto/S3UploadRequest.java @@ -0,0 +1,8 @@ +package com.example.api.aws.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.springframework.web.multipart.MultipartFile; + +public record S3UploadRequest(@NotNull MultipartFile multipartFile, @NotBlank String key) { +} diff --git a/src/main/java/com/example/api/aws/dto/UploadProfileRequest.java b/src/main/java/com/example/api/aws/dto/UploadProfileRequest.java new file mode 100644 index 00000000..8458a66a --- /dev/null +++ b/src/main/java/com/example/api/aws/dto/UploadProfileRequest.java @@ -0,0 +1,9 @@ +package com.example.api.aws.dto; + +import jakarta.validation.constraints.NotNull; +import org.springframework.web.multipart.MultipartFile; + +public record UploadProfileRequest( + @NotNull Long userId, + @NotNull MultipartFile multipartFile) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/aws/dto/UploadProfileResponse.java b/src/main/java/com/example/api/aws/dto/UploadProfileResponse.java new file mode 100644 index 00000000..fe100682 --- /dev/null +++ b/src/main/java/com/example/api/aws/dto/UploadProfileResponse.java @@ -0,0 +1,6 @@ +package com.example.api.aws.dto; + +import jakarta.validation.constraints.NotNull; + +public record UploadProfileResponse(@NotNull String path) { +} diff --git a/src/main/java/com/example/api/aws/service/S3Service.java b/src/main/java/com/example/api/aws/service/S3Service.java new file mode 100644 index 00000000..2e0037f6 --- /dev/null +++ b/src/main/java/com/example/api/aws/service/S3Service.java @@ -0,0 +1,93 @@ +package com.example.api.aws.service; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.AmazonS3Exception; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.example.api.account.repository.AccountRepository; +import com.example.api.aws.dto.S3UploadRequest; +import com.example.api.aws.dto.OldKeyRequest; +import com.example.api.aws.dto.UploadProfileRequest; +import com.example.api.aws.dto.UploadProfileResponse; +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import com.example.api.global.config.AmazonConfig; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.io.IOException; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class S3Service { + private final AmazonS3 amazonS3; + private final AmazonConfig amazonConfig; + private final AccountRepository accountRepository; + + @Transactional + public UploadProfileResponse upload(@Validated final UploadProfileRequest request) { + // 업로드 파일이 null 이라면 기본 프로필로 초기화 + if (initDefaultIfFileIsNull(request)) return new UploadProfileResponse(null); + + Optional userProfile = accountRepository.findProfileImageByAccountId(request.userId()); + userProfile.ifPresent(oldKey -> remove(new OldKeyRequest(oldKey))); + + String key = generateFileName(request); + String path = uploadToS3(new S3UploadRequest(request.multipartFile(), key)); + accountRepository.updateProfileImageByAccountId(key, request.userId()); // S3 업로드 이후 사용자 테이블 프로필 값 업데이트 + + return new UploadProfileResponse(path); + } + + private boolean initDefaultIfFileIsNull(final UploadProfileRequest request) { + if (request.multipartFile() == null || request.multipartFile().isEmpty()) { + accountRepository.updateProfileImageByAccountId(null, request.userId()); + String oldKey = "user-uploads/" + request.userId() + "/profile.png"; + remove(new OldKeyRequest(oldKey)); + return true; + } + return false; + } + + private String generateFileName(final UploadProfileRequest request) { + String contentType = request.multipartFile().getContentType(); + String fileExtension = contentType != null && contentType.contains("/") + ? "." + contentType.split("/")[1] + : ".png"; + return String.format("user-uploads/%d/profile%s", request.userId(), fileExtension); + } + + private String uploadToS3(final S3UploadRequest s3UploadRequest) { + try { + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(s3UploadRequest.multipartFile().getSize()); + metadata.setContentType(s3UploadRequest.multipartFile().getContentType()); + + amazonS3.putObject( + new PutObjectRequest( + amazonConfig.getBucket(), + s3UploadRequest.key(), + s3UploadRequest.multipartFile().getInputStream(), + metadata + ) + ); + + return amazonS3.getUrl(amazonConfig.getBucket(), s3UploadRequest.key()).toString(); + + } catch (IOException e) { + throw new BusinessException(ErrorCode.FILE_UPLOAD_FAILED); + } + } + + public void remove(final OldKeyRequest request) { + if (!amazonS3.doesObjectExist(amazonConfig.getBucket(), request.oldKey())) { + throw new AmazonS3Exception("Object " + request.oldKey() + " does not exist!"); + } + amazonS3.deleteObject(amazonConfig.getBucket(), request.oldKey()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/board/controller/BoardController.java b/src/main/java/com/example/api/board/controller/BoardController.java new file mode 100644 index 00000000..a0330352 --- /dev/null +++ b/src/main/java/com/example/api/board/controller/BoardController.java @@ -0,0 +1,56 @@ +package com.example.api.board.controller; + +import com.example.api.board.dto.request.EmployeeIdRequest; +import com.example.api.board.dto.response.Board; +import com.example.api.board.dto.response.CategoryDTO; +import com.example.api.board.dto.response.MyInfoDTO; +import com.example.api.board.service.BoardService; +import com.example.api.board.service.CategoryService; +import com.example.api.board.service.EmployeeService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class BoardController { + private final BoardService boardService; + private final CategoryService categoryService; + private final EmployeeService employeeService; + + @GetMapping("/api/v1/possible-board/form") + public Board findBoardByEmployeeId(@AuthenticationPrincipal final Long employeeId) { + EmployeeIdRequest employeeIdRequest = new EmployeeIdRequest(employeeId); + MyInfoDTO myInfoById = boardService.findMyInfoById(employeeIdRequest); + List categoryList = categoryService.getAllCategories(); + return new Board(myInfoById, categoryList); + } + + @PostMapping("/api/v1") + public ResponseEntity changeOpenStatus(@AuthenticationPrincipal final Long employeeId, @RequestParam ("openStatus") Boolean openStatus) { + EmployeeIdRequest employeeIdRequest = new EmployeeIdRequest(employeeId); + boolean updated = employeeService.changeOpenStatus(employeeIdRequest, openStatus); + if (updated) { + return ResponseEntity.ok("사용자 정보가 성공적으로 업데이트되었습니다."); + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("사용자를 찾을 수 없습니다."); + } + } + + @PostMapping("/api/v1/possible-board/submit") + public ResponseEntity submitBoard(@AuthenticationPrincipal final Long employeeId, @RequestBody MyInfoDTO myInfo) { + EmployeeIdRequest employeeIdRequest = new EmployeeIdRequest(employeeId); + employeeService.updateUserInfo(employeeIdRequest, myInfo); + MyInfoDTO myInfoById = boardService.findMyInfoById(employeeIdRequest); + List categoryList = categoryService.getAllCategories(); + return ResponseEntity.ok(new Board(myInfoById, categoryList)); + } +} diff --git a/src/main/java/com/example/api/board/dto/request/EmployeeIdRequest.java b/src/main/java/com/example/api/board/dto/request/EmployeeIdRequest.java new file mode 100644 index 00000000..c51f8a71 --- /dev/null +++ b/src/main/java/com/example/api/board/dto/request/EmployeeIdRequest.java @@ -0,0 +1,4 @@ +package com.example.api.board.dto.request; + +public record EmployeeIdRequest(Long employeeId) { +} diff --git a/src/main/java/com/example/api/board/dto/request/QueryPossibleEmployerRequest.java b/src/main/java/com/example/api/board/dto/request/QueryPossibleEmployerRequest.java new file mode 100644 index 00000000..0a811a1d --- /dev/null +++ b/src/main/java/com/example/api/board/dto/request/QueryPossibleEmployerRequest.java @@ -0,0 +1,11 @@ +package com.example.api.board.dto.request; + +import java.time.LocalDateTime; + +public record QueryPossibleEmployerRequest( + String name, + Integer age, + LocalDateTime possibleStartDateTime, + LocalDateTime possibleEndDateTime +) { +} diff --git a/src/main/java/com/example/api/board/dto/response/Board.java b/src/main/java/com/example/api/board/dto/response/Board.java new file mode 100644 index 00000000..78c77aa0 --- /dev/null +++ b/src/main/java/com/example/api/board/dto/response/Board.java @@ -0,0 +1,17 @@ +package com.example.api.board.dto.response; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +@EqualsAndHashCode +public class Board { + private MyInfoDTO myInfo; + private List categoryList; +} diff --git a/src/main/java/com/example/api/board/dto/response/CategoryDTO.java b/src/main/java/com/example/api/board/dto/response/CategoryDTO.java new file mode 100644 index 00000000..19d99dc4 --- /dev/null +++ b/src/main/java/com/example/api/board/dto/response/CategoryDTO.java @@ -0,0 +1,13 @@ +package com.example.api.board.dto.response; + +import lombok.*; + +@Getter +@Setter +@AllArgsConstructor +@EqualsAndHashCode +@NoArgsConstructor +public class CategoryDTO { + private Long categoryId; + private String categoryName; +} diff --git a/src/main/java/com/example/api/board/dto/response/ExternalCareerDTO.java b/src/main/java/com/example/api/board/dto/response/ExternalCareerDTO.java new file mode 100644 index 00000000..25d3d279 --- /dev/null +++ b/src/main/java/com/example/api/board/dto/response/ExternalCareerDTO.java @@ -0,0 +1,15 @@ +package com.example.api.board.dto.response; + + +import lombok.*; + +@Getter +@Setter +@AllArgsConstructor +@EqualsAndHashCode +@NoArgsConstructor +public class ExternalCareerDTO { + private long id; + private String Name; + private String period; +} diff --git a/src/main/java/com/example/api/board/dto/response/InnerCareerDTO.java b/src/main/java/com/example/api/board/dto/response/InnerCareerDTO.java new file mode 100644 index 00000000..d4e854df --- /dev/null +++ b/src/main/java/com/example/api/board/dto/response/InnerCareerDTO.java @@ -0,0 +1,29 @@ +package com.example.api.board.dto.response; + +import com.example.api.domain.Review; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter +@Setter +@EqualsAndHashCode +@NoArgsConstructor +public class InnerCareerDTO { + private String businessName; + private LocalDate workDate; + private String representationName; + private Review review; + + public InnerCareerDTO(String businessName, LocalDateTime startTime, String representationName, Review review) { + this.businessName = businessName; + this.workDate = startTime.toLocalDate(); + this.representationName = representationName; + this.review = review; + } +} diff --git a/src/main/java/com/example/api/board/dto/response/MyInfoDTO.java b/src/main/java/com/example/api/board/dto/response/MyInfoDTO.java new file mode 100644 index 00000000..441a1263 --- /dev/null +++ b/src/main/java/com/example/api/board/dto/response/MyInfoDTO.java @@ -0,0 +1,36 @@ +package com.example.api.board.dto.response; + +import lombok.*; + +import java.util.List; + +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class MyInfoDTO { + private String name; + private String nickname; + private int age; + private String sex; + private String email; + private String phone; + private List innerCarrerList; + private List externalCareerList; + private List possibleBoardList; + private List flavoredCategoryList; + private float starPoint; + private int workCount; + + public MyInfoDTO(String name, String nickname, int age, String sex, String email, String phone, float starPoint, int workCount) { + this.name = name; + this.nickname = nickname; + this.age = age; + this.sex = sex; + this.email = email; + this.phone = phone; + this.starPoint = starPoint; + this.workCount = workCount; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/board/dto/response/PossibleBoardDTO.java b/src/main/java/com/example/api/board/dto/response/PossibleBoardDTO.java new file mode 100644 index 00000000..b042af55 --- /dev/null +++ b/src/main/java/com/example/api/board/dto/response/PossibleBoardDTO.java @@ -0,0 +1,19 @@ +package com.example.api.board.dto.response; + +import lombok.*; +import java.time.LocalDateTime; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class PossibleBoardDTO { + private long id; + + @EqualsAndHashCode.Include + private LocalDateTime startTime; + + @EqualsAndHashCode.Include + private LocalDateTime endTime; +} diff --git a/src/main/java/com/example/api/board/dto/response/PossibleEmployeeResponse.java b/src/main/java/com/example/api/board/dto/response/PossibleEmployeeResponse.java new file mode 100644 index 00000000..551e6f92 --- /dev/null +++ b/src/main/java/com/example/api/board/dto/response/PossibleEmployeeResponse.java @@ -0,0 +1,13 @@ +package com.example.api.board.dto.response; + +import java.time.LocalDateTime; + +public record PossibleEmployeeResponse( + Long accountId, + String name, + Integer age, + String sex, + LocalDateTime possibleStartDateTime, + LocalDateTime possibleEndDateTime +) { +} diff --git a/src/main/java/com/example/api/board/dto/update/UpdateAccountConditionCommand.java b/src/main/java/com/example/api/board/dto/update/UpdateAccountConditionCommand.java new file mode 100644 index 00000000..04519732 --- /dev/null +++ b/src/main/java/com/example/api/board/dto/update/UpdateAccountConditionCommand.java @@ -0,0 +1,4 @@ +package com.example.api.board.dto.update; + +public interface UpdateAccountConditionCommand { +} diff --git a/src/main/java/com/example/api/board/dto/update/UpdateOpenStatusRequest.java b/src/main/java/com/example/api/board/dto/update/UpdateOpenStatusRequest.java new file mode 100644 index 00000000..a7b85c19 --- /dev/null +++ b/src/main/java/com/example/api/board/dto/update/UpdateOpenStatusRequest.java @@ -0,0 +1,6 @@ +package com.example.api.board.dto.update; + +import jakarta.validation.constraints.NotNull; + +public record UpdateOpenStatusRequest(@NotNull boolean openStatus) implements UpdateAccountConditionCommand { +} diff --git a/src/main/java/com/example/api/board/dto/update/UpdateUserInfoRequest.java b/src/main/java/com/example/api/board/dto/update/UpdateUserInfoRequest.java new file mode 100644 index 00000000..424cd813 --- /dev/null +++ b/src/main/java/com/example/api/board/dto/update/UpdateUserInfoRequest.java @@ -0,0 +1,11 @@ +package com.example.api.board.dto.update; + +public record UpdateUserInfoRequest( + String name, + String sex, + Integer age, + String phoneNumber, + String email, + String nickname +) implements UpdateAccountConditionCommand{ +} diff --git a/src/main/java/com/example/api/board/entitiy/update/UpdateAccountConditionHandler.java b/src/main/java/com/example/api/board/entitiy/update/UpdateAccountConditionHandler.java new file mode 100644 index 00000000..0cd26c13 --- /dev/null +++ b/src/main/java/com/example/api/board/entitiy/update/UpdateAccountConditionHandler.java @@ -0,0 +1,9 @@ +package com.example.api.board.entitiy.update; + +import com.example.api.board.dto.update.UpdateAccountConditionCommand; +import com.example.api.domain.Account; + +public interface UpdateAccountConditionHandler { + void update(final Account account, final UpdateAccountConditionCommand updateAccountConditionCommand); + boolean supports(final UpdateAccountConditionCommand updateAccountConditionCommand); +} diff --git a/src/main/java/com/example/api/board/entitiy/update/UpdateAccountConditionManager.java b/src/main/java/com/example/api/board/entitiy/update/UpdateAccountConditionManager.java new file mode 100644 index 00000000..dbd855a3 --- /dev/null +++ b/src/main/java/com/example/api/board/entitiy/update/UpdateAccountConditionManager.java @@ -0,0 +1,26 @@ +package com.example.api.board.entitiy.update; + +import com.example.api.board.dto.update.UpdateAccountConditionCommand; +import com.example.api.domain.Account; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UpdateAccountConditionManager { + private final List updateAccountConditionHandlers; + + @Transactional(propagation = Propagation.MANDATORY) + public void updateAccount(final Account account, final UpdateAccountConditionCommand command) { + UpdateAccountConditionHandler handler = updateAccountConditionHandlers.stream() + .filter(h -> h.supports(command)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("적합한 핸들러가 없음")); + + handler.update(account, command); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/board/entitiy/update/UpdateOpenStatusHandler.java b/src/main/java/com/example/api/board/entitiy/update/UpdateOpenStatusHandler.java new file mode 100644 index 00000000..45efb00a --- /dev/null +++ b/src/main/java/com/example/api/board/entitiy/update/UpdateOpenStatusHandler.java @@ -0,0 +1,19 @@ +package com.example.api.board.entitiy.update; + +import com.example.api.board.dto.update.UpdateAccountConditionCommand; +import com.example.api.board.dto.update.UpdateOpenStatusRequest; +import com.example.api.domain.Account; +import org.springframework.stereotype.Service; + +@Service +public class UpdateOpenStatusHandler implements UpdateAccountConditionHandler { + @Override + public void update(Account account, UpdateAccountConditionCommand updateAccountConditionCommand) { + account.updateOpenStatus((UpdateOpenStatusRequest) updateAccountConditionCommand); + } + + @Override + public boolean supports(UpdateAccountConditionCommand command) { + return command instanceof UpdateOpenStatusRequest; + } +} diff --git a/src/main/java/com/example/api/board/entitiy/update/UpdateUserInfoHandler.java b/src/main/java/com/example/api/board/entitiy/update/UpdateUserInfoHandler.java new file mode 100644 index 00000000..06717203 --- /dev/null +++ b/src/main/java/com/example/api/board/entitiy/update/UpdateUserInfoHandler.java @@ -0,0 +1,17 @@ +package com.example.api.board.entitiy.update; + +import com.example.api.board.dto.update.UpdateAccountConditionCommand; +import com.example.api.board.dto.update.UpdateUserInfoRequest; +import com.example.api.domain.Account; + +public class UpdateUserInfoHandler implements UpdateAccountConditionHandler { + @Override + public void update(Account account, UpdateAccountConditionCommand updateAccountConditionCommand) { + account.updateUserInfo((UpdateUserInfoRequest) updateAccountConditionCommand); + } + + @Override + public boolean supports(UpdateAccountConditionCommand command) { + return command instanceof UpdateUserInfoRequest; + } +} diff --git a/src/main/java/com/example/api/board/service/BoardService.java b/src/main/java/com/example/api/board/service/BoardService.java new file mode 100644 index 00000000..54c1b5eb --- /dev/null +++ b/src/main/java/com/example/api/board/service/BoardService.java @@ -0,0 +1,32 @@ +package com.example.api.board.service; + +import com.example.api.board.dto.request.EmployeeIdRequest; +import com.example.api.board.dto.response.MyInfoDTO; +import com.example.api.domain.repository.ExternalCareerRepository; +import com.example.api.domain.repository.FlavoredRepository; +import com.example.api.domain.repository.MyInfoRepository; +import com.example.api.domain.repository.OfferEmploymentRepository; +import com.example.api.possbileboard.PossibleBoardRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class BoardService { + private final MyInfoRepository myInfoRepository; + private final OfferEmploymentRepository offerEmploymentRepository; + private final ExternalCareerRepository externalCareerRepository; + private final PossibleBoardRepository possibleBoardRepository; + private final FlavoredRepository flavoredRepository; + + @Transactional(readOnly = true) + public MyInfoDTO findMyInfoById(final EmployeeIdRequest employeeIdRequest) { + MyInfoDTO myInfoDTOById = myInfoRepository.findMyInfoDTOById(employeeIdRequest.employeeId()); + myInfoDTOById.setInnerCarrerList(offerEmploymentRepository.findAllDTOByEmployeeId(employeeIdRequest.employeeId())); + myInfoDTOById.setExternalCareerList(externalCareerRepository.findAllDTOByEmployeeAccountId(employeeIdRequest.employeeId())); + myInfoDTOById.setPossibleBoardList(possibleBoardRepository.findAllDTOByEmployeeAccountId(employeeIdRequest.employeeId())); + myInfoDTOById.setFlavoredCategoryList(flavoredRepository.findAllCategoryDTOByEmployeeId(employeeIdRequest.employeeId())); + return myInfoDTOById; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/board/service/CategoryService.java b/src/main/java/com/example/api/board/service/CategoryService.java new file mode 100644 index 00000000..acc14687 --- /dev/null +++ b/src/main/java/com/example/api/board/service/CategoryService.java @@ -0,0 +1,25 @@ +package com.example.api.board.service; + +import com.example.api.board.dto.response.CategoryDTO; +import com.example.api.domain.repository.CategoryRepository; +import com.example.api.domain.Category; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class CategoryService { + private final CategoryRepository categoryRepository; + + @Transactional(readOnly = true) + public List getAllCategories() { + List categories = categoryRepository.findAll(); + return categories.stream() + .map(category -> new CategoryDTO(category.getCategoryId(), category.getCategoryName())) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/example/api/board/service/EmployeeService.java b/src/main/java/com/example/api/board/service/EmployeeService.java new file mode 100644 index 00000000..08fbeda4 --- /dev/null +++ b/src/main/java/com/example/api/board/service/EmployeeService.java @@ -0,0 +1,130 @@ +package com.example.api.board.service; + +import com.example.api.board.dto.request.EmployeeIdRequest; +import com.example.api.board.dto.response.CategoryDTO; +import com.example.api.board.dto.response.ExternalCareerDTO; +import com.example.api.board.dto.response.MyInfoDTO; +import com.example.api.board.dto.response.PossibleBoardDTO; +import com.example.api.board.dto.update.UpdateUserInfoRequest; +import com.example.api.board.entitiy.update.UpdateAccountConditionManager; +import com.example.api.domain.Account; +import com.example.api.domain.Category; +import com.example.api.domain.ExternalCareer; +import com.example.api.domain.Flavored; +import com.example.api.domain.PossibleBoard; +import com.example.api.domain.repository.EmployeeRepository; +import com.example.api.domain.repository.ExternalCareerRepository; +import com.example.api.domain.repository.FlavoredRepository; +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import com.example.api.possbileboard.PossibleBoardRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Repository +@RequiredArgsConstructor +public class EmployeeService { + private final EmployeeRepository employeeRepository; + private final ExternalCareerRepository externalCareerRepository; + private final FlavoredRepository flavoredRepository; + private final PossibleBoardRepository possibleBoardRepository; + private final UpdateAccountConditionManager updateAccountConditionManager; + + @Transactional + public Boolean changeOpenStatus(final EmployeeIdRequest employeeIdRequest, boolean openStatus) { + return employeeRepository.findByAccountId(employeeIdRequest.employeeId()).map(employee -> { + employee.setOpenStatus(openStatus); + employeeRepository.save(employee); + return true; + }).orElse(false); + } + + @Transactional + public void updateUserInfo(final EmployeeIdRequest employeeIdRequest, MyInfoDTO myInfo) { + try { + Account employee = employeeRepository.findByAccountId(employeeIdRequest.employeeId()).orElseThrow(); + updateAccountConditionManager.updateAccount(employee, getUpdateUserInfoRequest(myInfo)); + employeeRepository.save(employee); + updateExternalCareer(employee, myInfo.getExternalCareerList()); + updateFlavored(employee, myInfo.getFlavoredCategoryList()); + updatePossibleBoard(employee, myInfo.getPossibleBoardList()); + }catch (Exception e) { + throw new BusinessException(ErrorCode.NULL_USER); + } + } + private UpdateUserInfoRequest getUpdateUserInfoRequest(MyInfoDTO myInfo) { + return new UpdateUserInfoRequest( + myInfo.getName(), + myInfo.getSex(), + myInfo.getAge(), + myInfo.getPhone(), + myInfo.getEmail(), + myInfo.getNickname() + ); + } + public void updateExternalCareer(Account employee, List newExternalCareerList) { + List existList = externalCareerRepository.findAllByEmployeeAccountId(employee.getAccountId()); + Set newSet = new HashSet<>(newExternalCareerList); + + Set toDelete = existList.stream() + .filter(exist -> newSet.stream() + .noneMatch(newDto -> newDto.getName().equals(exist.getName()) && newDto.getPeriod().equals(exist.getPeriod()))) + .collect(Collectors.toSet()); + + Set toAdd = newExternalCareerList.stream() + .filter(dto -> existList.stream() + .noneMatch(exist -> exist.getName().equals(dto.getName()) && exist.getPeriod().equals(dto.getPeriod()))) + .map(dto -> new ExternalCareer(employee, dto.getName(), dto.getPeriod())) + .collect(Collectors.toSet()); + + externalCareerRepository.deleteAll(toDelete); + externalCareerRepository.saveAll(toAdd); + } + public void updateFlavored(Account employee, List newCategoryList) { + List existFlavored = flavoredRepository.findAllByEmployeeAccountId(employee.getAccountId()); + + Set newCategoryIds = newCategoryList.stream() + .map(CategoryDTO::getCategoryId) + .collect(Collectors.toSet()); + + Set toDelete = existFlavored.stream() + .filter(exist -> !newCategoryIds.contains(exist.getCategory().getCategoryId())) + .collect(Collectors.toSet()); + + Set toAdd = newCategoryList.stream() + .filter(dto -> existFlavored.stream() + .noneMatch(flavored -> flavored.getCategory().getCategoryId().equals(dto.getCategoryId()))) + .map(dto -> new Flavored(new Category(dto.getCategoryId(), dto.getCategoryName()), employee)) + .collect(Collectors.toSet()); + + flavoredRepository.deleteAll(toDelete); + flavoredRepository.saveAll(toAdd); + } + public void updatePossibleBoard(Account employee, List newPossibleBoard) { + List existPossibleBoard = possibleBoardRepository.findAllByEmployeeAccountId(employee.getAccountId()); + Set newSet = new HashSet<>(newPossibleBoard); + + Set toDelete = existPossibleBoard.stream() + .filter(exist -> newSet.stream() + .noneMatch(newDto -> newDto.getStartTime().equals(exist.getStartTime()) + && newDto.getEndTime().equals(exist.getEndTime()))) + .collect(Collectors.toSet()); + + Set ToAdd = newSet.stream() + .filter(newDto -> existPossibleBoard.stream() + .noneMatch(exist -> newDto.getStartTime().equals(exist.getStartTime()) + && newDto.getEndTime().equals(exist.getEndTime()))) + .map(dto -> new PossibleBoard(employee, dto.getStartTime(), dto.getEndTime())) + .collect(Collectors.toSet()); + + possibleBoardRepository.deleteAll(toDelete); + possibleBoardRepository.saveAll(ToAdd); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/business/BusinessQueryService.java b/src/main/java/com/example/api/business/BusinessQueryService.java new file mode 100644 index 00000000..daf1337a --- /dev/null +++ b/src/main/java/com/example/api/business/BusinessQueryService.java @@ -0,0 +1,35 @@ +package com.example.api.business; + +import com.example.api.business.dto.BusinessDetailsResponse; +import com.example.api.business.dto.BusinessOwner; +import com.example.api.business.dto.CategoryInfo; +import com.example.api.business.dto.QueryBusinessDetailCommand; +import com.example.api.domain.Account; +import com.example.api.domain.Business; +import com.example.api.domain.BusinessCategory; +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class BusinessQueryService { + private final BusinessRepository businessRepository; + + public BusinessDetailsResponse loadDetails(@Validated final QueryBusinessDetailCommand queryBusinessDetailCommand) { + final Business business = businessRepository.getDetails(queryBusinessDetailCommand.businessId()) + .orElseThrow(() -> new BusinessException("해당 비즈니스를 찾을 수 없습니다.", ErrorCode.BUSINESS_DOMAIN_EXCEPTION)); + final Account account = business.getEmployer(); + final List categoryInfos = business.getBusinessCategories().stream() + .map(BusinessCategory::getCategory) + .map(category -> new CategoryInfo(category.getCategoryId(), category.getCategoryName())) + .toList(); + final BusinessOwner owner = new BusinessOwner(account.getAccountId(), account.getName()); + return new BusinessDetailsResponse(business.getBusinessName(), business.getBusinessId(), owner, business.getLocation(), categoryInfos); + } +} diff --git a/src/main/java/com/example/api/business/BusinessRepository.java b/src/main/java/com/example/api/business/BusinessRepository.java new file mode 100644 index 00000000..b583c712 --- /dev/null +++ b/src/main/java/com/example/api/business/BusinessRepository.java @@ -0,0 +1,20 @@ +package com.example.api.business; + +import com.example.api.domain.Business; +import com.example.api.employer.controller.dto.EmployerBusinessesRequest; +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 BusinessRepository extends JpaRepository { + @Query("SELECT b FROM Business b JOIN FETCH b.employer JOIN FETCH b.businessCategories WHERE b.businessId = :businessId") + Optional getDetails(@Param("businessId") final Long businessId); + + @Query("select new com.example.api.employer.controller.dto.EmployerBusinessesRequest(b.businessName, b.location) from Business b where b.employer.accountId = :employerId order by b.location.id") + List findBusinessesByEmployeeId(@Param("employerId")final Long employerId); +} \ No newline at end of file diff --git a/src/main/java/com/example/api/business/BusinessService.java b/src/main/java/com/example/api/business/BusinessService.java new file mode 100644 index 00000000..cf98a7be --- /dev/null +++ b/src/main/java/com/example/api/business/BusinessService.java @@ -0,0 +1,49 @@ +package com.example.api.business; + +import com.example.api.business.dto.AddBusinessCommand; +import com.example.api.business.dto.ModifyBusinessCommand; +import com.example.api.business.update.BusinessUpdateManager; +import com.example.api.domain.Business; +import com.example.api.domain.BusinessCategory; +import com.example.api.domain.Category; +import com.example.api.domain.repository.BusinessCategoryRepository; +import com.example.api.domain.repository.CategoryRepository; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +@Service +@Slf4j +@RequiredArgsConstructor +public class BusinessService { + private final BusinessRepository businessRepository; + private final BusinessCategoryRepository businessCategoryRepository; + private final BusinessUpdateManager businessUpdateManager; + private final CategoryRepository categoryRepository; + + @Transactional + public void updateBusiness(@Validated final ModifyBusinessCommand command) { + final Business targetBusiness = businessRepository.findById(command.businessId()) + .orElseThrow(); + businessUpdateManager.update(targetBusiness, command); + } + + @Transactional + public void addBusiness(@Validated final AddBusinessCommand command) { + final Business business = new Business(command.businessName(), command.location(), command.representationName()); + final List businessCategories = loadCategories(command.categoryIds(), business); + businessRepository.save(business); + businessCategoryRepository.saveAll(businessCategories); + } + + private List loadCategories(final List categoryIds, final Business business) { + final List categories = categoryRepository.findAllById(categoryIds); + return categories.stream() + .map(category -> new BusinessCategory(business, category)) + .toList(); + } +} diff --git a/src/main/java/com/example/api/business/controller/BusinessController.java b/src/main/java/com/example/api/business/controller/BusinessController.java new file mode 100644 index 00000000..55d9eb7a --- /dev/null +++ b/src/main/java/com/example/api/business/controller/BusinessController.java @@ -0,0 +1,77 @@ +package com.example.api.business.controller; + +import com.example.api.account.entity.Location; +import com.example.api.business.BusinessQueryService; +import com.example.api.business.BusinessService; +import com.example.api.business.dto.AddBusinessCommand; +import com.example.api.business.dto.ModifyBusinessCommand; +import com.example.api.business.dto.QueryBusinessDetailCommand; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/business") +@RequiredArgsConstructor +public class BusinessController { + private final BusinessService businessService; + private final BusinessQueryService businessQueryService; + + @GetMapping + public ResponseEntity getMyBusiness( + @RequestParam final Long businessId + ) { + return ResponseEntity.ok(businessQueryService.loadDetails(new QueryBusinessDetailCommand(businessId))); + } + + @PutMapping + public ResponseEntity modifyMyBusiness( + @RequestBody final ModifyBusinessRequest request + ) { + final ModifyBusinessCommand command = request.toCommand(); + businessService.updateBusiness(command); + return ResponseEntity.ok().build(); + } + + @PostMapping + public ResponseEntity addBusiness( + @RequestBody final AddBusinessRequest request + ) { + final AddBusinessCommand command = request.toCommand(); + businessService.addBusiness(command); + return ResponseEntity.ok().build(); + } + + record AddBusinessRequest( + Long requestMemberId, + String businessName, + Location location, + List categoryIds, + String representationName + ) { + AddBusinessCommand toCommand() { + return new AddBusinessCommand(requestMemberId, businessName, location, categoryIds, representationName); + } + } + + record ModifyBusinessRequest( + @NotNull + Long businessId, + String businessName, + Location location, + String representationName, + List categoryId + ) { + ModifyBusinessCommand toCommand() { + return new ModifyBusinessCommand(businessId, businessName, location, representationName, categoryId); + } + } +} diff --git a/src/main/java/com/example/api/business/domain/BusinessLocation.java b/src/main/java/com/example/api/business/domain/BusinessLocation.java new file mode 100644 index 00000000..51990e53 --- /dev/null +++ b/src/main/java/com/example/api/business/domain/BusinessLocation.java @@ -0,0 +1,13 @@ +package com.example.api.business.domain; + +import com.example.api.account.entity.Location; +import lombok.Getter; + +@Getter +public class BusinessLocation { + private final Location location; + + public BusinessLocation(Location location) { + this.location = location; + } +} diff --git a/src/main/java/com/example/api/business/domain/BusinessName.java b/src/main/java/com/example/api/business/domain/BusinessName.java new file mode 100644 index 00000000..d1432f8c --- /dev/null +++ b/src/main/java/com/example/api/business/domain/BusinessName.java @@ -0,0 +1,26 @@ +package com.example.api.business.domain; + +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import lombok.Getter; + +@Getter +public class BusinessName { + private static final int NAME_MAX_LENGTH = 20; + private static final int NAME_MIN_LENGTH = 1; + private final String name; + + public BusinessName(final String name) { + validateLength(name); + this.name = name; + } + + private void validateLength(final String name) { + if (NAME_MIN_LENGTH > name.length()) { + throw new BusinessException(NAME_MIN_LENGTH + " 이상이여야 합니다.", ErrorCode.BUSINESS_DOMAIN_EXCEPTION); + } + if (NAME_MAX_LENGTH < name.length()) { + throw new BusinessException(NAME_MAX_LENGTH + " 이히이여야 합니다.", ErrorCode.BUSINESS_DOMAIN_EXCEPTION); + } + } +} diff --git a/src/main/java/com/example/api/business/domain/BusinessRepresentationName.java b/src/main/java/com/example/api/business/domain/BusinessRepresentationName.java new file mode 100644 index 00000000..e5cd05ab --- /dev/null +++ b/src/main/java/com/example/api/business/domain/BusinessRepresentationName.java @@ -0,0 +1,12 @@ +package com.example.api.business.domain; + +import lombok.Getter; + +@Getter +public class BusinessRepresentationName { + private final String representationName; + + public BusinessRepresentationName(String representationName) { + this.representationName = representationName; + } +} diff --git a/src/main/java/com/example/api/business/dto/AddBusinessCommand.java b/src/main/java/com/example/api/business/dto/AddBusinessCommand.java new file mode 100644 index 00000000..565ae71a --- /dev/null +++ b/src/main/java/com/example/api/business/dto/AddBusinessCommand.java @@ -0,0 +1,16 @@ +package com.example.api.business.dto; + +import com.example.api.account.entity.Location; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record AddBusinessCommand( + @NotNull + Long requestMemberId, + String businessName, + Location location, + List categoryIds, + String representationName + +) { +} diff --git a/src/main/java/com/example/api/business/dto/BusinessDetailsResponse.java b/src/main/java/com/example/api/business/dto/BusinessDetailsResponse.java new file mode 100644 index 00000000..f036b2b4 --- /dev/null +++ b/src/main/java/com/example/api/business/dto/BusinessDetailsResponse.java @@ -0,0 +1,13 @@ +package com.example.api.business.dto; + +import com.example.api.account.entity.Location; + +import java.util.List; + +public record BusinessDetailsResponse( + String businessName, + Long businessId, + BusinessOwner owner, + Location location, + List categoryInfos) { +} diff --git a/src/main/java/com/example/api/business/dto/BusinessOwner.java b/src/main/java/com/example/api/business/dto/BusinessOwner.java new file mode 100644 index 00000000..d1d66d72 --- /dev/null +++ b/src/main/java/com/example/api/business/dto/BusinessOwner.java @@ -0,0 +1,7 @@ +package com.example.api.business.dto; + +public record BusinessOwner( + Long accountId, + String name +) { +} diff --git a/src/main/java/com/example/api/business/dto/CategoryInfo.java b/src/main/java/com/example/api/business/dto/CategoryInfo.java new file mode 100644 index 00000000..0c11c0c5 --- /dev/null +++ b/src/main/java/com/example/api/business/dto/CategoryInfo.java @@ -0,0 +1,7 @@ +package com.example.api.business.dto; + +public record CategoryInfo( + Long categoryid, + String categoryname +) { +} diff --git a/src/main/java/com/example/api/business/dto/ModifyBusinessCommand.java b/src/main/java/com/example/api/business/dto/ModifyBusinessCommand.java new file mode 100644 index 00000000..840694f7 --- /dev/null +++ b/src/main/java/com/example/api/business/dto/ModifyBusinessCommand.java @@ -0,0 +1,15 @@ +package com.example.api.business.dto; + +import com.example.api.account.entity.Location; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record ModifyBusinessCommand( + @NotNull + Long businessId, + String businessName, + Location location, + String representationName, + List categoryIds +) { +} diff --git a/src/main/java/com/example/api/business/dto/QueryBusinessDetailCommand.java b/src/main/java/com/example/api/business/dto/QueryBusinessDetailCommand.java new file mode 100644 index 00000000..4f8e9eae --- /dev/null +++ b/src/main/java/com/example/api/business/dto/QueryBusinessDetailCommand.java @@ -0,0 +1,6 @@ +package com.example.api.business.dto; + +public record QueryBusinessDetailCommand( + Long businessId +) { +} diff --git a/src/main/java/com/example/api/business/update/BusinessUpdateHandler.java b/src/main/java/com/example/api/business/update/BusinessUpdateHandler.java new file mode 100644 index 00000000..cc6de5d1 --- /dev/null +++ b/src/main/java/com/example/api/business/update/BusinessUpdateHandler.java @@ -0,0 +1,8 @@ +package com.example.api.business.update; + +import com.example.api.business.dto.ModifyBusinessCommand; +import com.example.api.domain.Business; + +interface BusinessUpdateHandler { + void update(final Business business, final ModifyBusinessCommand command); +} diff --git a/src/main/java/com/example/api/business/update/BusinessUpdateManager.java b/src/main/java/com/example/api/business/update/BusinessUpdateManager.java new file mode 100644 index 00000000..0ea7849e --- /dev/null +++ b/src/main/java/com/example/api/business/update/BusinessUpdateManager.java @@ -0,0 +1,18 @@ +package com.example.api.business.update; + +import com.example.api.business.dto.ModifyBusinessCommand; +import com.example.api.domain.Business; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class BusinessUpdateManager { + private final List updateHandlers; + + public void update(final Business business, final ModifyBusinessCommand command) { + updateHandlers.stream() + .forEach(businessUpdateHandler -> businessUpdateHandler.update(business, command)); + } +} diff --git a/src/main/java/com/example/api/business/update/UpdateBusinessCategoriesHandlerImpl.java b/src/main/java/com/example/api/business/update/UpdateBusinessCategoriesHandlerImpl.java new file mode 100644 index 00000000..bd8a9a92 --- /dev/null +++ b/src/main/java/com/example/api/business/update/UpdateBusinessCategoriesHandlerImpl.java @@ -0,0 +1,48 @@ +package com.example.api.business.update; + +import com.example.api.business.dto.ModifyBusinessCommand; +import com.example.api.domain.Business; +import com.example.api.domain.BusinessCategory; +import com.example.api.domain.Category; +import com.example.api.domain.repository.BusinessCategoryRepository; +import com.example.api.domain.repository.CategoryRepository; +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(propagation = Propagation.MANDATORY) +@RequiredArgsConstructor +public class UpdateBusinessCategoriesHandlerImpl implements BusinessUpdateHandler { + private final CategoryRepository categoryRepository; + private final BusinessCategoryRepository businessCategoryRepository; + + @Override + public void update(Business business, ModifyBusinessCommand command) { + if (Objects.isNull(command.categoryIds())) { + return; + } + final List businessCategories = findRelatedCategories(command.categoryIds()).stream() + .map(category -> new BusinessCategory(business, category)) + .collect(Collectors.toList()); + businessCategoryRepository.saveAll(businessCategories); + } + + private List findRelatedCategories(final List categoryIds) { + final List categories = categoryRepository.findAllById(categoryIds); + validate(categories.size(), categoryIds.size()); + return categories; + } + + private void validate(final int requestCategorySize, final int foundedSize) { + if (foundedSize != requestCategorySize) { + throw new BusinessException("카테고리를 찾을 수 없습니다.", ErrorCode.CATEGORY_EXCEPTION); + } + } +} diff --git a/src/main/java/com/example/api/business/update/UpdateBusinessLocationHandler.java b/src/main/java/com/example/api/business/update/UpdateBusinessLocationHandler.java new file mode 100644 index 00000000..3ae94cc7 --- /dev/null +++ b/src/main/java/com/example/api/business/update/UpdateBusinessLocationHandler.java @@ -0,0 +1,21 @@ +package com.example.api.business.update; + +import com.example.api.business.domain.BusinessLocation; +import com.example.api.business.dto.ModifyBusinessCommand; +import com.example.api.domain.Business; +import java.util.Objects; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(propagation = Propagation.MANDATORY) +class UpdateBusinessLocationHandler implements BusinessUpdateHandler{ + @Override + public void update(Business business, ModifyBusinessCommand command) { + if (Objects.nonNull(command.location())) { + final BusinessLocation location = new BusinessLocation(command.location()); + business.setLocation(location.getLocation()); + } + } +} diff --git a/src/main/java/com/example/api/business/update/UpdateBusinessNameHandler.java b/src/main/java/com/example/api/business/update/UpdateBusinessNameHandler.java new file mode 100644 index 00000000..a4b116f2 --- /dev/null +++ b/src/main/java/com/example/api/business/update/UpdateBusinessNameHandler.java @@ -0,0 +1,21 @@ +package com.example.api.business.update; + +import com.example.api.business.domain.BusinessName; +import com.example.api.business.dto.ModifyBusinessCommand; +import com.example.api.domain.Business; +import java.util.Objects; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(propagation = Propagation.MANDATORY) +class UpdateBusinessNameHandler implements BusinessUpdateHandler { + @Override + public void update(Business business, ModifyBusinessCommand command) { + if (Objects.nonNull(command.businessName())) { + final BusinessName businessName = new BusinessName(command.businessName()); + business.setBusinessName(businessName.getName()); + } + } +} diff --git a/src/main/java/com/example/api/business/update/UpdateBusinessRepresentationNameHandler.java b/src/main/java/com/example/api/business/update/UpdateBusinessRepresentationNameHandler.java new file mode 100644 index 00000000..079406ec --- /dev/null +++ b/src/main/java/com/example/api/business/update/UpdateBusinessRepresentationNameHandler.java @@ -0,0 +1,21 @@ +package com.example.api.business.update; + +import com.example.api.business.domain.BusinessRepresentationName; +import com.example.api.business.dto.ModifyBusinessCommand; +import com.example.api.domain.Business; +import java.util.Objects; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(propagation = Propagation.MANDATORY) +public class UpdateBusinessRepresentationNameHandler implements BusinessUpdateHandler { + @Override + public void update(Business business, ModifyBusinessCommand command) { + if (Objects.nonNull(command.representationName())) { + final BusinessRepresentationName representationName = new BusinessRepresentationName(command.representationName()); + business.setRepresentationName(representationName.getRepresentationName()); + } + } +} diff --git a/src/main/java/com/example/api/chat/controller/ChatController.java b/src/main/java/com/example/api/chat/controller/ChatController.java new file mode 100644 index 00000000..5840f583 --- /dev/null +++ b/src/main/java/com/example/api/chat/controller/ChatController.java @@ -0,0 +1,42 @@ +package com.example.api.chat.controller; + +import com.example.api.chat.controller.dto.request.ChatSendRequest; +import com.example.api.chat.controller.dto.request.ReadRequest; +import com.example.api.chat.controller.dto.request.UserIdRequest; +import com.example.api.chat.controller.dto.response.ChatSummaryResponse; +import com.example.api.chat.service.ChatService; +import com.example.api.domain.Chat; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class ChatController { + private final ChatService chatService; + + @MessageMapping("/send") + public void sendMessage(@Payload ChatSendRequest chatRequest) { + chatService.sendChat(chatRequest); + } + + @MessageMapping("/read") + public void readMessage(@Payload ReadRequest readRequest) { + chatService.readChats(readRequest); + } + + @GetMapping("/chat/summaries") + public ResponseEntity getChatSummaries(@RequestBody UserIdRequest userIdRequest) { + return ResponseEntity.ok(chatService.getChatSummaries(userIdRequest)); + } + + @GetMapping("/chat/room/{roomId}/chats") + public ResponseEntity> getMessages(@PathVariable("roomId") Long chatRoomId, + @RequestParam(value = "lastChatId", required = false) String chatId) { + return ResponseEntity.ok(chatService.getChats(chatRoomId,chatId)); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/chat/controller/dto/request/ChatSendRequest.java b/src/main/java/com/example/api/chat/controller/dto/request/ChatSendRequest.java new file mode 100644 index 00000000..e9de2004 --- /dev/null +++ b/src/main/java/com/example/api/chat/controller/dto/request/ChatSendRequest.java @@ -0,0 +1,4 @@ +package com.example.api.chat.controller.dto.request; + +public record ChatSendRequest(Long roomId, Long senderId, Long receiverId, String content) { +} diff --git a/src/main/java/com/example/api/chat/controller/dto/request/ReadRequest.java b/src/main/java/com/example/api/chat/controller/dto/request/ReadRequest.java new file mode 100644 index 00000000..9e14c097 --- /dev/null +++ b/src/main/java/com/example/api/chat/controller/dto/request/ReadRequest.java @@ -0,0 +1,4 @@ +package com.example.api.chat.controller.dto.request; + +public record ReadRequest(Long roomId, Long userId) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/chat/controller/dto/request/UserIdRequest.java b/src/main/java/com/example/api/chat/controller/dto/request/UserIdRequest.java new file mode 100644 index 00000000..18e2316c --- /dev/null +++ b/src/main/java/com/example/api/chat/controller/dto/request/UserIdRequest.java @@ -0,0 +1,4 @@ +package com.example.api.chat.controller.dto.request; + +public record UserIdRequest(Long userId) { +} diff --git a/src/main/java/com/example/api/chat/controller/dto/response/ChatSummaryResponse.java b/src/main/java/com/example/api/chat/controller/dto/response/ChatSummaryResponse.java new file mode 100644 index 00000000..29c669bc --- /dev/null +++ b/src/main/java/com/example/api/chat/controller/dto/response/ChatSummaryResponse.java @@ -0,0 +1,9 @@ +package com.example.api.chat.controller.dto.response; + +import com.example.api.chat.dto.ChatSummary; +import com.example.api.domain.ChatRoom; + +import java.util.List; + +public record ChatSummaryResponse(List chatRooms, List chatSummaries) { +} diff --git a/src/main/java/com/example/api/chat/controller/dto/response/ReadResponse.java b/src/main/java/com/example/api/chat/controller/dto/response/ReadResponse.java new file mode 100644 index 00000000..17334c33 --- /dev/null +++ b/src/main/java/com/example/api/chat/controller/dto/response/ReadResponse.java @@ -0,0 +1,4 @@ +package com.example.api.chat.controller.dto.response; + +public record ReadResponse(Long readBy) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/chat/dto/ChatSummary.java b/src/main/java/com/example/api/chat/dto/ChatSummary.java new file mode 100644 index 00000000..f23f3896 --- /dev/null +++ b/src/main/java/com/example/api/chat/dto/ChatSummary.java @@ -0,0 +1,6 @@ +package com.example.api.chat.dto; + +import java.util.Date; + +public record ChatSummary(Long roomId, String lastMessageContent, Date lastMessageTime, Long numberOfUnreadMessages) { +} diff --git a/src/main/java/com/example/api/chat/repository/ChatRepository.java b/src/main/java/com/example/api/chat/repository/ChatRepository.java new file mode 100644 index 00000000..197c67e8 --- /dev/null +++ b/src/main/java/com/example/api/chat/repository/ChatRepository.java @@ -0,0 +1,9 @@ +package com.example.api.chat.repository; + +import com.example.api.domain.Chat; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ChatRepository extends MongoRepository, CustomChatRepository { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/chat/repository/ChatRoomRepository.java b/src/main/java/com/example/api/chat/repository/ChatRoomRepository.java new file mode 100644 index 00000000..b178b6b4 --- /dev/null +++ b/src/main/java/com/example/api/chat/repository/ChatRoomRepository.java @@ -0,0 +1,16 @@ +package com.example.api.chat.repository; + +import com.example.api.domain.ChatRoom; +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; + +@Repository +public interface ChatRoomRepository extends JpaRepository { + @Query("select c from ChatRoom c join c.offerEmployment oe " + + "where oe.employee.accountId = :userId or oe.business.employer.accountId = :userId") + List findByUserId(@Param("userId") Long userId); +} diff --git a/src/main/java/com/example/api/chat/repository/CustomChatRepository.java b/src/main/java/com/example/api/chat/repository/CustomChatRepository.java new file mode 100644 index 00000000..2468694a --- /dev/null +++ b/src/main/java/com/example/api/chat/repository/CustomChatRepository.java @@ -0,0 +1,12 @@ +package com.example.api.chat.repository; + +import com.example.api.chat.dto.ChatSummary; +import com.example.api.domain.Chat; + +import java.util.List; + +public interface CustomChatRepository { + void markChatsAsRead(Long chatRoomId, Long readBy); + List findChats(Long chatRoomId, String lastChatId); + List aggregateChatSummaries(List chatRoomIds, Long memberId); +} diff --git a/src/main/java/com/example/api/chat/repository/CustomChatRepositoryImpl.java b/src/main/java/com/example/api/chat/repository/CustomChatRepositoryImpl.java new file mode 100644 index 00000000..a3deda94 --- /dev/null +++ b/src/main/java/com/example/api/chat/repository/CustomChatRepositoryImpl.java @@ -0,0 +1,78 @@ +package com.example.api.chat.repository; + +import com.example.api.chat.dto.ChatSummary; +import com.example.api.domain.Chat; +import lombok.RequiredArgsConstructor; +import org.bson.types.ObjectId; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationOperation; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.aggregation.ConditionalOperators; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class CustomChatRepositoryImpl implements CustomChatRepository { + private final MongoTemplate mongoTemplate; + + + @Override + public void markChatsAsRead(Long chatRoomId, Long readBy) { + Query query = new Query(Criteria.where("roomId").is(chatRoomId) + .and("receiverId").is(readBy) + .and("isRead").is(false)); + Update update = new Update(); + update.set("isRead", true); + mongoTemplate.updateMulti(query, update, Chat.class); + } + + @Override + public List findChats(Long chatRoomID, String lastChatId) { + Query query = new Query( + Criteria.where("roomId").is(chatRoomID) + ).with(Sort.by(Sort.Direction.DESC, "_id")).limit(100); + + if (lastChatId != null) { + query.addCriteria(Criteria.where("_id").lt(new ObjectId(lastChatId))); + } + + return mongoTemplate.find(query, Chat.class); + } + + + + @Override + public List aggregateChatSummaries(List roomIds, Long memberId) { + Criteria matchCriteria = Criteria.where("roomId").in(roomIds); + AggregationOperation match = Aggregation.match(matchCriteria); + + AggregationOperation sort = Aggregation.sort(Sort.Direction.DESC, "sendTime"); + + AggregationOperation group = Aggregation.group("roomId") + .first("roomId").as("roomId") + .first("content").as("lastChatContent") + .first("sendTime").as("lastChatTime") + .sum(ConditionalOperators + .when(new Criteria().andOperator( + Criteria.where("receiverId").is(memberId), + Criteria.where("isRead").is(false) + )) + .then(1) + .otherwise(0)) + .as("numberOfUnreadChats"); + + Aggregation aggregation = Aggregation.newAggregation(match, sort, group); + + AggregationResults results = mongoTemplate.aggregate( + aggregation, "chat", ChatSummary.class); + return new ArrayList<>(results.getMappedResults()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/chat/service/ChatService.java b/src/main/java/com/example/api/chat/service/ChatService.java new file mode 100644 index 00000000..4dd73ed4 --- /dev/null +++ b/src/main/java/com/example/api/chat/service/ChatService.java @@ -0,0 +1,61 @@ +package com.example.api.chat.service; + +import com.example.api.chat.controller.dto.request.UserIdRequest; +import com.example.api.chat.controller.dto.request.ChatSendRequest; +import com.example.api.chat.controller.dto.request.ReadRequest; +import com.example.api.chat.controller.dto.response.ChatSummaryResponse; +import com.example.api.chat.dto.ChatSummary; +import com.example.api.chat.repository.ChatRepository; +import com.example.api.chat.repository.ChatRoomRepository; +import com.example.api.chat.service.model.ChatSender; +import com.example.api.domain.Chat; +import com.example.api.domain.ChatRoom; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ChatService { + private final ChatRepository chatRepository; + private final ChatRoomRepository chatRoomRepository; + private final ChatSender chatSender; + + @Transactional + public String sendChat(ChatSendRequest request) { + Chat savedChat = saveChat(request); + chatSender.send(savedChat); + return "메세지 전송 성공~"; + } + + private Chat saveChat(ChatSendRequest request) { + Chat chat = Chat.from(request); + return chatRepository.save(chat); + } + + @Transactional + public String readChats(ReadRequest request) { + chatRepository.markChatsAsRead(request.roomId(), request.userId()); + chatSender.sendReadResponse(request); + return "메세지 읽기 성공~"; + } + + @Transactional(readOnly = true) + public ChatSummaryResponse getChatSummaries(UserIdRequest requestUserId) { + List chatRooms = chatRoomRepository.findByUserId(requestUserId.userId()); + List chatRoomIds = chatRooms.stream().map(ChatRoom::getChatRoomId).toList(); + List chatSummaries = chatRepository.aggregateChatSummaries(chatRoomIds, requestUserId.userId()); + return new ChatSummaryResponse(chatRooms, chatSummaries); + } + + @Transactional(readOnly = true) + public List getChats(Long chatRoomId, String lastChatId) { + return chatRepository.findChats(chatRoomId,lastChatId); + } + + public List getChatRooms(Long userId) { + return chatRoomRepository.findByUserId(userId); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/chat/service/model/ChatSender.java b/src/main/java/com/example/api/chat/service/model/ChatSender.java new file mode 100644 index 00000000..e827d378 --- /dev/null +++ b/src/main/java/com/example/api/chat/service/model/ChatSender.java @@ -0,0 +1,23 @@ +package com.example.api.chat.service.model; + +import com.example.api.chat.controller.dto.request.ReadRequest; +import com.example.api.chat.controller.dto.response.ReadResponse; +import com.example.api.domain.Chat; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ChatSender { + private final SimpMessagingTemplate messagingTemplate; + + public void send(Chat chat) { + messagingTemplate.convertAndSend("/room/"+ chat.getRoomId(), chat); + } + + public void sendReadResponse(ReadRequest request) { + ReadResponse readResponse = new ReadResponse(request.userId()); + messagingTemplate.convertAndSend("/room/" + request.roomId(), readResponse); + } +} diff --git a/src/main/java/com/example/api/contracts/ContractMapper.java b/src/main/java/com/example/api/contracts/ContractMapper.java new file mode 100644 index 00000000..17c2b95c --- /dev/null +++ b/src/main/java/com/example/api/contracts/ContractMapper.java @@ -0,0 +1,21 @@ +package com.example.api.contracts; + +import com.example.api.domain.Contract; +import com.example.api.domain.OfferEmployment; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(propagation = Propagation.MANDATORY) +class ContractMapper { + public Contract notYetSucceeded(final OfferEmployment offerEmployment) { + return new Contract( + offerEmployment, + offerEmployment.getSuggestStartTime(), + offerEmployment.getSuggestEndTime(), + offerEmployment.getSuggestHourlyPay(), + false + ); + } +} diff --git a/src/main/java/com/example/api/contracts/ContractRepository.java b/src/main/java/com/example/api/contracts/ContractRepository.java new file mode 100644 index 00000000..0e2252ca --- /dev/null +++ b/src/main/java/com/example/api/contracts/ContractRepository.java @@ -0,0 +1,45 @@ +package com.example.api.contracts; + +import com.example.api.contracts.dto.BusinessInfoDTO; +import com.example.api.contracts.dto.EmployeeInfoDTO; +import com.example.api.domain.Contract; +import java.util.Optional; +import com.example.api.reviewavailable.dto.ReviewAvailableResponse; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ContractRepository extends JpaRepository { + @Query("select new com.example.api.contracts.dto." + + "BusinessInfoDTO(b.businessName, b.representationName, c.contractStartTime, c.contractEndTime, b.location, e.phoneNumber, c.updatedDate) " + + "from Contract c " + + "join c.offerEmployment oe " + + "join oe.business b " + + "join oe.employee e on e.accountId = b.employer.accountId " + + "where c.contractId = :contractId") + BusinessInfoDTO findBusinessDTOByContractId(@Param("contractId") long contractId); + + @Query("select new com.example.api.contracts.dto." + + "EmployeeInfoDTO(e.name, e.phoneNumber, e.starPoint, e.workCount) " + + "from Contract c " + + "join c.offerEmployment oe " + + "join oe.employee e " + + "where c.contractId = :contractId") + EmployeeInfoDTO findEmployeeDTOByContractId(@Param("contractId") long contractId); + + + @Query("SELECT c FROM Contract c JOIN FETCH c.offerEmployment JOIN FETCH c.offerEmployment.business.employer WHERE c.contractId = :contractId") + Optional loadContractWithOfferEmployment(@Param("contractId") final Long contractId); + + @Query("select new com.example.api.reviewavailable.dto." + + "ReviewAvailableResponse(e.accountId, e.name) " + + "from Contract c " + + "join c.offerEmployment oe " + + "join oe.employee e " + + "join oe.business b " + + "where b.businessId = :businessId and c.contractSucceeded = true") + List findAvailableReviewsByBusinessId(@Param("businessId") Long businessId); + +} diff --git a/src/main/java/com/example/api/contracts/ContractReviewService.java b/src/main/java/com/example/api/contracts/ContractReviewService.java new file mode 100644 index 00000000..efd3a941 --- /dev/null +++ b/src/main/java/com/example/api/contracts/ContractReviewService.java @@ -0,0 +1,35 @@ +package com.example.api.contracts; + +import com.example.api.contracts.dto.AddReviewCommand; +import com.example.api.domain.Business; +import com.example.api.domain.Contract; +import com.example.api.domain.Review; +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import com.example.api.review.ReviewRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +@Service +@RequiredArgsConstructor +public class ContractReviewService { + private final ContractRepository contractRepository; + private final ReviewRepository reviewRepository; + + @Transactional + public void saveReview(@Validated final AddReviewCommand command) { + final Contract contract = contractRepository.loadContractWithOfferEmployment(command.contractId()) + .orElseThrow(); + validateContractOwner(command.requestMemberId(), contract.getOfferEmployment().getBusiness()); + final Review review = new Review(command.reviewScore(), command.reviewContent(), contract); + reviewRepository.save(review); + } + + private void validateContractOwner(final Long requestMemberId, final Business business) { + if (!business.getEmployer().getAccountId().equals(requestMemberId)) { + throw new BusinessException("본인의 계약에만 리뷰가 가능합니다", ErrorCode.CONTRACT_EXCEPTION); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/contracts/ContractService.java b/src/main/java/com/example/api/contracts/ContractService.java new file mode 100644 index 00000000..1001f920 --- /dev/null +++ b/src/main/java/com/example/api/contracts/ContractService.java @@ -0,0 +1,77 @@ +package com.example.api.contracts; + +import com.example.api.chat.repository.ChatRoomRepository; +import com.example.api.contracts.dto.*; +import com.example.api.contracts.update.UpdateContractConditionManager; +import com.example.api.domain.ChatRoom; +import com.example.api.domain.Contract; +import com.example.api.domain.OfferEmployment; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +@Service +@RequiredArgsConstructor +public class ContractService { + private final OfferRepository offerRepository; + private final ContractRepository contractRepository; + private final ContractMapper contractMapper; + private final ChatRoomRepository chatRoomRepository; + private final UpdateContractConditionManager updateContractConditionManager; + + @Transactional(readOnly = true) + public List getAllRelatedSuggests(final QueryAllSuggestsForMeCommand allSuggestsForMeCommand) { + return offerRepository.queryEmployersSuggests(allSuggestsForMeCommand.employeeId()); + } + + @Transactional + public void acceptSuggest(@Validated final AcceptSuggestCommand acceptSuggestCommand) { + final OfferEmployment offerEmployment = loadOffer(acceptSuggestCommand.suggestId()); + offerEmployment.succeeded(); + + final Contract contract = contractMapper.notYetSucceeded(offerEmployment); + contractRepository.save(contract); + } + + @Transactional + public void createChatRoom(@Validated final AcceptSuggestCommand acceptSuggestCommand) { + final OfferEmployment offerEmployment = loadOffer(acceptSuggestCommand.suggestId()); + createChatRoom(offerEmployment); + } + + @Transactional + public void updateContract(@Validated final UpdateContractConditionCommand updateContractConditionCommand) { + final Contract contract = loadContract(updateContractConditionCommand.contractId()); + updateContractConditionManager.updateContract(contract, updateContractConditionCommand); + } + + @Transactional + public void acceptContract(@Validated final AcceptContractCommand acceptContractCommand) { + final Contract contract = loadContract(acceptContractCommand.contractId()); + contract.succeed(); + } + + private Contract loadContract(final Long contractId) { + return contractRepository.findById(contractId) + .orElseThrow(); + } + + private OfferEmployment loadOffer(final Long offerId) { + return offerRepository.findById(offerId) + .orElseThrow(); + } + + private void createChatRoom(final OfferEmployment offer) { + ChatRoom chatRoom = new ChatRoom(offer); + chatRoomRepository.save(chatRoom); + } + + @Transactional(readOnly = true) + public ContractDTO getContractInfo(final AcceptContractCommand contractStatusCommand) { + BusinessInfoDTO businessDTO = contractRepository.findBusinessDTOByContractId(contractStatusCommand.contractId()); + EmployeeInfoDTO employeeDTO = contractRepository.findEmployeeDTOByContractId(contractStatusCommand.contractId()); + return new ContractDTO(businessDTO, employeeDTO); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/contracts/OfferRepository.java b/src/main/java/com/example/api/contracts/OfferRepository.java new file mode 100644 index 00000000..f1d1236c --- /dev/null +++ b/src/main/java/com/example/api/contracts/OfferRepository.java @@ -0,0 +1,15 @@ +package com.example.api.contracts; + +import com.example.api.contracts.dto.SuggestedBusinessResponse; +import com.example.api.domain.OfferEmployment; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface OfferRepository extends JpaRepository { + @Query("SELECT new com.example.api.contracts.dto.SuggestedBusinessResponse(offer.business.businessId, offer.suggestStartTime, offer.suggestEndTime, offer.suggestHourlyPay, offer.suggestReaded, offer.suggestSucceeded) " + + "FROM OfferEmployment offer " + + "WHERE offer.employee.accountId = :employeeId") + List queryEmployersSuggests(@Param("employeeId") final Long employeeId); +} diff --git a/src/main/java/com/example/api/contracts/ReviewQueryService.java b/src/main/java/com/example/api/contracts/ReviewQueryService.java new file mode 100644 index 00000000..ca35d9b3 --- /dev/null +++ b/src/main/java/com/example/api/contracts/ReviewQueryService.java @@ -0,0 +1,29 @@ +package com.example.api.contracts; + +import com.example.api.contracts.dto.QueryEmployersReviewCommand; +import com.example.api.contracts.dto.ReviewResponse; +import com.example.api.domain.Review; +import java.util.List; + +import com.example.api.review.ReviewRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ReviewQueryService { + private final ReviewRepository reviewRepository; + + @Transactional(readOnly = true) + public List loadReviewsWithEmployerId(final QueryEmployersReviewCommand command) { + final List reviews = reviewRepository.loadReviewsByEmployerId(command.employerId()); + return reviews.stream() + .map(review -> new ReviewResponse( + review.getReviewId(), + review.getContract().getContractId(), + review.getReviewContent(), + review.getReviewStarPoint())) + .toList(); + } +} diff --git a/src/main/java/com/example/api/contracts/controller/ContractController.java b/src/main/java/com/example/api/contracts/controller/ContractController.java new file mode 100644 index 00000000..72a29a02 --- /dev/null +++ b/src/main/java/com/example/api/contracts/controller/ContractController.java @@ -0,0 +1,72 @@ +package com.example.api.contracts.controller; + +import com.example.api.contracts.dto.ContractDTO; +import com.example.api.contracts.ContractService; +import com.example.api.contracts.dto.AcceptContractCommand; +import com.example.api.contracts.dto.AcceptSuggestCommand; +import com.example.api.contracts.dto.QueryAllSuggestsForMeCommand; +import com.example.api.contracts.dto.SuggestedBusinessResponse; +import com.example.api.contracts.dto.UpdateContractConditionCommand; +import com.example.api.contracts.dto.UpdateContractConditionRequest; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +class ContractController { + private final ContractService contractService; + + @GetMapping("/api/v1/contracts/employment-suggests") + public ResponseEntity> getAllSuggest( + @RequestParam(required = true) final Long employeeId + ) { + final QueryAllSuggestsForMeCommand queryAllSuggestsForMeCommand = new QueryAllSuggestsForMeCommand(employeeId); + final List suggestedBusinesses = contractService.getAllRelatedSuggests( + queryAllSuggestsForMeCommand); + return ResponseEntity.ok(suggestedBusinesses); + } + + @PostMapping("/api/v1/contracts/suggests/{suggestId}/accept") + public ResponseEntity acceptContractContact( + @RequestBody final AcceptSuggestCommand acceptSuggestCommand + ) { + contractService.acceptSuggest(acceptSuggestCommand); + return ResponseEntity.ok(null); + } + + @PostMapping("/api/v1/contracts/suggests/{suggestId}/chatroom") + public ResponseEntity createChatRoom( + @RequestBody final AcceptSuggestCommand acceptSuggestCommand + ) { + contractService.createChatRoom(acceptSuggestCommand); + return ResponseEntity.ok(null); + } + + @PutMapping("/api/v1/contracts/{contractId}") + public ResponseEntity updateContractCondition( + @PathVariable(required = true) final Long contractId, + @RequestBody final UpdateContractConditionRequest updateContractConditionRequest + ) { + final UpdateContractConditionCommand updateCommand = updateContractConditionRequest.toCommand(contractId); + contractService.updateContract(updateCommand); + return ResponseEntity.ok(null); + } + + @PostMapping("/api/v1/contracts/{contractId}/accepts") + public ResponseEntity acceptContract( + @PathVariable(required = true) final Long contractId + ) { + final AcceptContractCommand acceptContractCommand = new AcceptContractCommand(contractId); + contractService.acceptContract(acceptContractCommand); + return ResponseEntity.ok(null); + } + + @GetMapping("/api/v1/contract/{contractId}/status") + public ResponseEntity getContractInfo(@PathVariable(required = true) final Long contractId) { + final AcceptContractCommand contractStatusCommand = new AcceptContractCommand(contractId); + ContractDTO contractDTO = contractService.getContractInfo(contractStatusCommand); + return ResponseEntity.ok(contractDTO); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/contracts/controller/ContractReviewController.java b/src/main/java/com/example/api/contracts/controller/ContractReviewController.java new file mode 100644 index 00000000..13a20031 --- /dev/null +++ b/src/main/java/com/example/api/contracts/controller/ContractReviewController.java @@ -0,0 +1,62 @@ +package com.example.api.contracts.controller; + +import com.example.api.contracts.ReviewQueryService; +import com.example.api.contracts.ContractReviewService; +import com.example.api.contracts.dto.AddReviewCommand; +import com.example.api.contracts.dto.QueryEmployersReviewCommand; +import com.example.api.contracts.dto.ReviewResponse; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.hibernate.validator.constraints.Range; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/contracts") +public class ContractReviewController { + private final ContractReviewService contractReviewService; + private final ReviewQueryService reviewQueryService; + + + /** + * @param requestMemberId 고용자 ( employer ID ) 가 본인이 작성한 리뷰 목록 가져오기 + * @return + */ + @GetMapping("/review/my") + public ResponseEntity> getMyReview( + @AuthenticationPrincipal final Long requestMemberId + ) { + final QueryEmployersReviewCommand command = new QueryEmployersReviewCommand(requestMemberId); + return ResponseEntity.ok(reviewQueryService.loadReviewsWithEmployerId(command)); + } + + @PostMapping("/review") + public ResponseEntity addReview( + @RequestBody @Valid final AddReviewRequest request, + @AuthenticationPrincipal final Long memberId + ) { + final AddReviewCommand command = request.toCommand(memberId); + contractReviewService.saveReview(command); + return ResponseEntity.ok(null); + } + + record AddReviewRequest( + @NotNull + Long contractId, + @Range(min = 0, max = 5) + Integer reviewScore, + String reviewContent + ) { + AddReviewCommand toCommand(final Long requestMemberId) { + return new AddReviewCommand(requestMemberId, contractId, reviewContent, reviewScore); + } + } +} diff --git a/src/main/java/com/example/api/contracts/dto/AcceptContractCommand.java b/src/main/java/com/example/api/contracts/dto/AcceptContractCommand.java new file mode 100644 index 00000000..19f53692 --- /dev/null +++ b/src/main/java/com/example/api/contracts/dto/AcceptContractCommand.java @@ -0,0 +1,9 @@ +package com.example.api.contracts.dto; + +import org.springframework.lang.NonNull; + +public record AcceptContractCommand( + @NonNull + Long contractId +) { +} diff --git a/src/main/java/com/example/api/contracts/dto/AcceptSuggestCommand.java b/src/main/java/com/example/api/contracts/dto/AcceptSuggestCommand.java new file mode 100644 index 00000000..1578b90c --- /dev/null +++ b/src/main/java/com/example/api/contracts/dto/AcceptSuggestCommand.java @@ -0,0 +1,9 @@ +package com.example.api.contracts.dto; + +import org.springframework.lang.NonNull; + +public record AcceptSuggestCommand( + @NonNull + Long suggestId +) { +} diff --git a/src/main/java/com/example/api/contracts/dto/AddReviewCommand.java b/src/main/java/com/example/api/contracts/dto/AddReviewCommand.java new file mode 100644 index 00000000..fbec28cf --- /dev/null +++ b/src/main/java/com/example/api/contracts/dto/AddReviewCommand.java @@ -0,0 +1,9 @@ +package com.example.api.contracts.dto; + +public record AddReviewCommand( + Long requestMemberId, + Long contractId, + String reviewContent, + Integer reviewScore +) { +} diff --git a/src/main/java/com/example/api/contracts/dto/BusinessInfoDTO.java b/src/main/java/com/example/api/contracts/dto/BusinessInfoDTO.java new file mode 100644 index 00000000..7ea7942d --- /dev/null +++ b/src/main/java/com/example/api/contracts/dto/BusinessInfoDTO.java @@ -0,0 +1,21 @@ +package com.example.api.contracts.dto; + +import com.example.api.account.entity.Location; +import lombok.*; + +import java.time.LocalDateTime; + +@EqualsAndHashCode +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class BusinessInfoDTO { + private String businessName; + private String representationName; + private LocalDateTime startTime; + private LocalDateTime endTime; + private Location location; + private String businessPhone; + private LocalDateTime signedDate; +} diff --git a/src/main/java/com/example/api/contracts/dto/ContractDTO.java b/src/main/java/com/example/api/contracts/dto/ContractDTO.java new file mode 100644 index 00000000..a72fa9a7 --- /dev/null +++ b/src/main/java/com/example/api/contracts/dto/ContractDTO.java @@ -0,0 +1,13 @@ +package com.example.api.contracts.dto; + +import lombok.*; + +@EqualsAndHashCode +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class ContractDTO { + private BusinessInfoDTO businessInfo; + private EmployeeInfoDTO employeeInfo; +} diff --git a/src/main/java/com/example/api/contracts/dto/EmployeeInfoDTO.java b/src/main/java/com/example/api/contracts/dto/EmployeeInfoDTO.java new file mode 100644 index 00000000..dc59675c --- /dev/null +++ b/src/main/java/com/example/api/contracts/dto/EmployeeInfoDTO.java @@ -0,0 +1,15 @@ +package com.example.api.contracts.dto; + +import lombok.*; + +@EqualsAndHashCode +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class EmployeeInfoDTO { + private String employeeName; + private String employeePhone; + private float starPoint; + private int workCount; +} diff --git a/src/main/java/com/example/api/contracts/dto/QueryAllSuggestsForMeCommand.java b/src/main/java/com/example/api/contracts/dto/QueryAllSuggestsForMeCommand.java new file mode 100644 index 00000000..9a330d89 --- /dev/null +++ b/src/main/java/com/example/api/contracts/dto/QueryAllSuggestsForMeCommand.java @@ -0,0 +1,6 @@ +package com.example.api.contracts.dto; + +public record QueryAllSuggestsForMeCommand( + Long employeeId +) { +} diff --git a/src/main/java/com/example/api/contracts/dto/QueryEmployersReviewCommand.java b/src/main/java/com/example/api/contracts/dto/QueryEmployersReviewCommand.java new file mode 100644 index 00000000..5fdd9775 --- /dev/null +++ b/src/main/java/com/example/api/contracts/dto/QueryEmployersReviewCommand.java @@ -0,0 +1,6 @@ +package com.example.api.contracts.dto; + +public record QueryEmployersReviewCommand( + Long employerId +) { +} diff --git a/src/main/java/com/example/api/contracts/dto/ReviewResponse.java b/src/main/java/com/example/api/contracts/dto/ReviewResponse.java new file mode 100644 index 00000000..60b87883 --- /dev/null +++ b/src/main/java/com/example/api/contracts/dto/ReviewResponse.java @@ -0,0 +1,9 @@ +package com.example.api.contracts.dto; + +public record ReviewResponse( + Long reviewId, + Long contractId, + String reviewContent, + Integer reviewScore +) { +} diff --git a/src/main/java/com/example/api/contracts/dto/SuggestedBusinessResponse.java b/src/main/java/com/example/api/contracts/dto/SuggestedBusinessResponse.java new file mode 100644 index 00000000..8f1dacd8 --- /dev/null +++ b/src/main/java/com/example/api/contracts/dto/SuggestedBusinessResponse.java @@ -0,0 +1,14 @@ +package com.example.api.contracts.dto; + +import java.time.LocalDateTime; + +public record SuggestedBusinessResponse( + Long businessId, + LocalDateTime suggestStartDateTime, + LocalDateTime suggestEndDateTime, + Integer suggestPartTimePayment, + Boolean suggestChecked, + Boolean suggestAccepted + +) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/contracts/dto/UpdateContractConditionCommand.java b/src/main/java/com/example/api/contracts/dto/UpdateContractConditionCommand.java new file mode 100644 index 00000000..b661bcb1 --- /dev/null +++ b/src/main/java/com/example/api/contracts/dto/UpdateContractConditionCommand.java @@ -0,0 +1,13 @@ +package com.example.api.contracts.dto; + +import java.time.LocalDateTime; +import org.springframework.lang.NonNull; + +public record UpdateContractConditionCommand( + @NonNull + Long contractId, + LocalDateTime suggestStartDateTime, + LocalDateTime suggestEndDateTime, + Integer suggestHourlyPayment +) { +} diff --git a/src/main/java/com/example/api/contracts/dto/UpdateContractConditionRequest.java b/src/main/java/com/example/api/contracts/dto/UpdateContractConditionRequest.java new file mode 100644 index 00000000..d08bdeab --- /dev/null +++ b/src/main/java/com/example/api/contracts/dto/UpdateContractConditionRequest.java @@ -0,0 +1,18 @@ +package com.example.api.contracts.dto; + +import java.time.LocalDateTime; + +public record UpdateContractConditionRequest( + LocalDateTime suggestStartDateTime, + LocalDateTime suggestEndDateTime, + Integer suggestHourlyPayment +) { + public UpdateContractConditionCommand toCommand(final Long contractId) { + return new UpdateContractConditionCommand( + contractId, + this.suggestStartDateTime, + this.suggestEndDateTime, + this.suggestHourlyPayment + ); + } +} diff --git a/src/main/java/com/example/api/contracts/update/UpdateContractConditionHandler.java b/src/main/java/com/example/api/contracts/update/UpdateContractConditionHandler.java new file mode 100644 index 00000000..db337ba7 --- /dev/null +++ b/src/main/java/com/example/api/contracts/update/UpdateContractConditionHandler.java @@ -0,0 +1,8 @@ +package com.example.api.contracts.update; + +import com.example.api.contracts.dto.UpdateContractConditionCommand; +import com.example.api.domain.Contract; + +interface UpdateContractConditionHandler { + void update(final Contract contract, final UpdateContractConditionCommand updateContractConditionCommand); +} diff --git a/src/main/java/com/example/api/contracts/update/UpdateContractConditionManager.java b/src/main/java/com/example/api/contracts/update/UpdateContractConditionManager.java new file mode 100644 index 00000000..1109f72d --- /dev/null +++ b/src/main/java/com/example/api/contracts/update/UpdateContractConditionManager.java @@ -0,0 +1,21 @@ +package com.example.api.contracts.update; + +import com.example.api.contracts.dto.UpdateContractConditionCommand; +import com.example.api.domain.Contract; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UpdateContractConditionManager { + private final List updateContractConditionHandlers; + + @Transactional(propagation = Propagation.MANDATORY) + public void updateContract(final Contract contract, final UpdateContractConditionCommand updateContractConditionCommand) { + updateContractConditionHandlers.stream() + .forEach(updateContractHandler -> updateContractHandler.update(contract, updateContractConditionCommand)); + } +} diff --git a/src/main/java/com/example/api/contracts/update/UpdateContractHourlyPaymentHandler.java b/src/main/java/com/example/api/contracts/update/UpdateContractHourlyPaymentHandler.java new file mode 100644 index 00000000..d68457e5 --- /dev/null +++ b/src/main/java/com/example/api/contracts/update/UpdateContractHourlyPaymentHandler.java @@ -0,0 +1,17 @@ +package com.example.api.contracts.update; + +import com.example.api.contracts.dto.UpdateContractConditionCommand; +import com.example.api.domain.Contract; +import java.util.Objects; +import org.springframework.stereotype.Service; + +@Service +class UpdateContractHourlyPaymentHandler implements UpdateContractConditionHandler { + @Override + public void update(final Contract contract, final UpdateContractConditionCommand updateContractConditionCommand) { + final Integer payment = updateContractConditionCommand.suggestHourlyPayment(); + if (Objects.nonNull(payment)) { + contract.updateHourlyPayment(payment); + } + } +} diff --git a/src/main/java/com/example/api/contracts/update/UpdateContractRangeHandler.java b/src/main/java/com/example/api/contracts/update/UpdateContractRangeHandler.java new file mode 100644 index 00000000..cd7caabb --- /dev/null +++ b/src/main/java/com/example/api/contracts/update/UpdateContractRangeHandler.java @@ -0,0 +1,23 @@ +package com.example.api.contracts.update; + +import com.example.api.contracts.dto.UpdateContractConditionCommand; +import com.example.api.domain.Contract; +import java.util.Objects; +import org.springframework.stereotype.Service; + +@Service +class UpdateContractRangeHandler implements UpdateContractConditionHandler { + @Override + public void update(final Contract contract, final UpdateContractConditionCommand updateCommand) { + if (Objects.nonNull(updateCommand.suggestStartDateTime())) { + contract.updateStartDateTime(updateCommand.suggestStartDateTime()); + } + if (Objects.nonNull(updateCommand.suggestEndDateTime())) { + contract.updateEndDateTime(updateCommand.suggestEndDateTime()); + } + + if (contract.isValidContractRangeTime()) { + throw new IllegalArgumentException(); + } + } +} diff --git a/src/main/java/com/example/api/domain/Account.java b/src/main/java/com/example/api/domain/Account.java index 5d4369ba..f8040442 100644 --- a/src/main/java/com/example/api/domain/Account.java +++ b/src/main/java/com/example/api/domain/Account.java @@ -1,21 +1,24 @@ package com.example.api.domain; +import com.example.api.account.entity.Nationality; +import com.example.api.account.entity.UserRole; +import com.example.api.auth.dto.LoginUserRequest; +import com.example.api.board.dto.update.UpdateOpenStatusRequest; +import com.example.api.board.dto.update.UpdateUserInfoRequest; import jakarta.persistence.*; import lombok.Getter; -import lombok.Setter; -import java.util.Date; +import java.util.Collection; @Entity @Getter -@Setter +@AttributeOverride(name = "createdDate", column = @Column(name = "ACCOUNT_REGISTERED_DATETIME")) @Table(name = "ACCOUNT") public class Account extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ACCOUNT_UNIQUE_ID") private Long accountId; - @Column(name = "ACCOUNT_ID") private String loginId; @Column(name = "ACCOUNT_PASSWORD") @@ -24,22 +27,107 @@ public class Account extends BaseEntity { private String name; @Column(name = "ACCOUNT_NICKNAME") private String nickname; + @Column(name = "ACCOUNT_PHONE_NUMBER") + private String phoneNumber; + @Column(name = "ACCOUNT_EMAIL") + private String email; + @Column(name = "ACCOUNT_NATIONALITY") + @Enumerated(EnumType.STRING) + private Nationality nationality; + @ElementCollection(targetClass = UserRole.class) + @CollectionTable(name = "AUTHORITY", joinColumns = @JoinColumn(name = "ACCOUNT_UNIQUE_ID"), foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + @Enumerated(EnumType.STRING) + private Collection roles; @Column(name = "ACCOUNT_SEX") private String sex; @Column(name = "ACCOUNT_AGE") private int age; - @Column(name = "ACCOUNT_PHONE_NUMBER") - private String phoneNumber; - @Column(name = "ACCOUNT_REGISTERED_DATETIME") - private Date registeredDatetime; @Column(name = "ACCOUNT_PROFILE_IMAGE") private String profileImage; - @Column(name = "ACCOUNT_EMAIL") - private String email; @Column(name = "ACCOUNT_STAR_RATING") private float starPoint; @Column(name = "ACCOUNT_WORK_COUNT") private int workCount; + @Column(name = "ACCOUNT_OPEN_STATUS") + private boolean openStatus; @Column(name = "ACCOUNT_DELETED", columnDefinition = "BOOLEAN DEFAULT false") private boolean deleted = false; + @Column(name = "ACCOUNT_EMAIL_RECEIVABLE", columnDefinition = "BOOLEAN DEFAULT false") + private boolean emailReceivable = false; + + public Account() { + } + + public Account(String name, String email, Collection roles) { + this.name = name; + this.email = email; + this.roles = roles; + } + + public Account(String loginId, String password, String name, String nickname, String phoneNumber, String email, Nationality nationality, Collection roles, final Boolean emailReceivable) { + this.loginId = loginId; + this.password = password; + this.name = name; + this.nickname = nickname; + this.email = email; + this.phoneNumber = phoneNumber; + this.nationality = nationality; + this.roles = roles; + this.starPoint = 0.0f; + this.workCount = 0; + this.openStatus = true; + this.emailReceivable = emailReceivable; + } + + public Account(String loginId, String password, String email, String phoneNumber, Nationality nationality, Collection roles) { + this.loginId = loginId; + this.password = password; + this.email = email; + this.phoneNumber = phoneNumber; + this.nationality = nationality; + this.roles = roles; + } + + public Account(int age, boolean deleted, String email, String loginId, String name, Nationality nationality, String nickname, boolean openStatus, String password, String phoneNumber, String profileImage, Collection roles, String sex, float starPoint, int workCount) { + this.age = age; + this.deleted = deleted; + this.email = email; + this.loginId = loginId; + this.name = name; + this.nationality = nationality; + this.nickname = nickname; + this.openStatus = openStatus; + this.password = password; + this.phoneNumber = phoneNumber; + this.profileImage = profileImage; + this.roles = roles; + this.sex = sex; + this.starPoint = starPoint; + this.workCount = workCount; + } + + public LoginUserRequest getLoginUser(){ + return new LoginUserRequest(accountId); + } + + public void updateOpenStatus(UpdateOpenStatusRequest request){ + this.openStatus = request.openStatus(); + } + + public void updateUserInfo(UpdateUserInfoRequest request){ + this.name = request.name(); + this.sex = request.sex(); + this.age = request.age(); + this.phoneNumber = request.phoneNumber(); + this.email = request.email(); + this.nickname = request.nickname(); + } + + public void setDeleted(boolean deleted){ + this.deleted = deleted; + } + + public void setOpenStatus(boolean openStatus) { + this.openStatus = openStatus; + } } \ No newline at end of file diff --git a/src/main/java/com/example/api/domain/Announcement.java b/src/main/java/com/example/api/domain/Announcement.java index a5549d04..c1aaea22 100644 --- a/src/main/java/com/example/api/domain/Announcement.java +++ b/src/main/java/com/example/api/domain/Announcement.java @@ -1,18 +1,20 @@ package com.example.api.domain; import jakarta.persistence.*; +import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.NonNull; import lombok.Setter; @Entity @Getter -@Setter +@EqualsAndHashCode(callSuper = false) @Table(name = "ANNOUNCEMENT") public class Announcement extends BaseEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long announcementId; - private String announcementTitle; private String announcementType; private String announcementContent; @@ -20,5 +22,13 @@ public class Announcement extends BaseEntity { @Column(columnDefinition = "int DEFAULT 0") private int viewCount; -} + public Announcement() {} + public Announcement(Long announcementId, String announcementTitle, String announcementType, String announcementContent, int viewCount) { + this.announcementId = announcementId; + this.announcementTitle = announcementTitle; + this.announcementType = announcementType; + this.announcementContent = announcementContent; + this.viewCount = viewCount; + } +} diff --git a/src/main/java/com/example/api/domain/BaseEntity.java b/src/main/java/com/example/api/domain/BaseEntity.java index eb354548..25f3a19f 100644 --- a/src/main/java/com/example/api/domain/BaseEntity.java +++ b/src/main/java/com/example/api/domain/BaseEntity.java @@ -23,4 +23,4 @@ public class BaseEntity { @LastModifiedDate @Column(name = "UPDATED_AT") private LocalDateTime updatedDate; -} +} \ No newline at end of file diff --git a/src/main/java/com/example/api/domain/Business.java b/src/main/java/com/example/api/domain/Business.java index e83fc075..2a77873d 100644 --- a/src/main/java/com/example/api/domain/Business.java +++ b/src/main/java/com/example/api/domain/Business.java @@ -1,27 +1,29 @@ package com.example.api.domain; +import com.example.api.account.entity.Location; import jakarta.persistence.*; +import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.Setter; +import lombok.NoArgsConstructor; import java.time.LocalDate; import java.util.ArrayList; -import java.util.Date; import java.util.List; import static jakarta.persistence.FetchType.*; @Entity @Getter -@Setter +@EqualsAndHashCode(callSuper = false) @Table(name = "BUSINESS") +@NoArgsConstructor public class Business extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long businessId; @ManyToOne(fetch = LAZY) - @JoinColumn(name = "BUSINESS_EMPLOYER_ID") + @JoinColumn(name = "BUSINESS_EMPLOYER_ID", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private Account employer; @OneToMany(mappedBy = "business") @@ -30,8 +32,9 @@ public class Business extends BaseEntity { @Column(name = "BUSINESS_NAME") private String businessName; - @Column(name = "BUSINESS_LOCATION") - private String location; + @OneToOne(fetch = LAZY) + @JoinColumn(name = "BUSINESS_LOCATION", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + private Location location; private String representationName; @@ -41,4 +44,44 @@ public class Business extends BaseEntity { @Column(name = "BUSINESS_REGISTRATION_NUMBER") private String registrationNumber; -} + public void setBusinessName(String businessName) { + this.businessName = businessName; + } + + public void setLocation(Location location) { + this.location = location; + } + + public void setRepresentationName(String representationName) { + this.representationName = representationName; + } + + public Business(String businessName, Location location, String representationName) { + this.businessName = businessName; + this.location = location; + this.representationName = representationName; + } + + public Business(Account user, String businessRegistrationNumber, String businessName, String representationName, String businessOpenDate, Location location) { + this.employer = user; + this.registrationNumber = businessRegistrationNumber; + this.businessName = businessName; + this.representationName = representationName; + this.openDate = LocalDate.parse(businessOpenDate); + this.location = location; + } + + public Business(String businessName, Location location, String representationName, Account employer, LocalDate openDate, String registrationNumber) { + this.businessName = businessName; + this.location = location; + this.representationName = representationName; + this.employer = employer; + this.openDate = openDate; + this.registrationNumber = registrationNumber; + } + + public Business(Account employer, String businessName) { + this.employer = employer; + this.businessName = businessName; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/domain/BusinessCategory.java b/src/main/java/com/example/api/domain/BusinessCategory.java index 84b612a9..ce7f1bb0 100644 --- a/src/main/java/com/example/api/domain/BusinessCategory.java +++ b/src/main/java/com/example/api/domain/BusinessCategory.java @@ -1,17 +1,17 @@ package com.example.api.domain; import jakarta.persistence.*; +import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.Setter; - -import java.util.List; +import lombok.NoArgsConstructor; import static jakarta.persistence.FetchType.*; @Entity @Getter -@Setter +@EqualsAndHashCode(callSuper = false) @Table(name = "BUSINESS_CATEGORY") +@NoArgsConstructor public class BusinessCategory extends BaseEntity{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -19,11 +19,15 @@ public class BusinessCategory extends BaseEntity{ private Long id; @ManyToOne(fetch = LAZY) - @JoinColumn(name = "BUSINESS_ID") + @JoinColumn(name = "BUSINESS_ID", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private Business business; @ManyToOne(fetch = LAZY) - @JoinColumn(name = "CATEGOREY_ID") + @JoinColumn(name = "CATEGOREY_ID", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private Category category; -} + public BusinessCategory(Business business, Category category) { + this.business = business; + this.category = category; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/domain/Category.java b/src/main/java/com/example/api/domain/Category.java index 0fa2957a..c229dcd8 100644 --- a/src/main/java/com/example/api/domain/Category.java +++ b/src/main/java/com/example/api/domain/Category.java @@ -1,6 +1,7 @@ package com.example.api.domain; import jakarta.persistence.*; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -9,14 +10,26 @@ @Entity @Getter -@Setter +@EqualsAndHashCode(callSuper = false) @Table(name = "CATEGORY") public class Category extends BaseEntity{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "CATEGORY_ID") private Long categoryId; + @Column(name = "CATEGORY_NAME") private String categoryName; -} + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ACCOUNT_UNIQUE_ID") + private Account account; + + public Category() { + } + + public Category(Long categoryId, String categoryName) { + this.categoryId = categoryId; + this.categoryName = categoryName; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/domain/Chat.java b/src/main/java/com/example/api/domain/Chat.java index 8d55967c..3f4f6658 100644 --- a/src/main/java/com/example/api/domain/Chat.java +++ b/src/main/java/com/example/api/domain/Chat.java @@ -1,43 +1,50 @@ package com.example.api.domain; +import com.example.api.chat.controller.dto.request.ChatSendRequest; import jakarta.persistence.*; import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.Setter; +import org.springframework.data.mongodb.core.mapping.Document; -import java.time.LocalDateTime; +import java.util.Date; -import static jakarta.persistence.FetchType.*; - -@Entity @Getter -@Setter @EqualsAndHashCode -@Table(name = "CHAT") +@Document(collection = "chat") public class Chat { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long chatId; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "CHAT_ROOM_ID") - private ChatRoom chatRoom; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "ACCOUNT_UNIQUE_ID") - private Account account; - - private String chatContent; - - @Column(name = "CHAT_DELETED", columnDefinition = "BOOLEAN DEFAULT false") - private boolean deleted; + private String id; + private String content; + private Long roomId; + private Long senderId; + private Long receiverId; + private Date sendTime; + private Boolean isRead; + + protected Chat() { + } - @Column(name = "CHAT_REGISTER_DATE") - private LocalDateTime chatRegisterDate; + public static Chat from(ChatSendRequest chatSendRequest){ + Chat chat = new Chat(); + chat.content = chatSendRequest.content(); + chat.roomId = chatSendRequest.roomId(); + chat.senderId = chatSendRequest.senderId(); + chat.receiverId = chatSendRequest.receiverId(); + chat.sendTime = new Date(); + chat.isRead = false; + return chat; + } - @PrePersist - protected void onCreate() { - this.chatRegisterDate = LocalDateTime.now(); + @Override + public String toString() { + return "Message{" + + "id=" + id + + ", content='" + content + '\'' + + ", roomId=" + roomId + + ", senderId=" + senderId + + ", receiverId=" + receiverId + + ", sendTime=" + sendTime + + ", isRead=" + isRead + + '}'; } -} +} \ No newline at end of file diff --git a/src/main/java/com/example/api/domain/ChatRoom.java b/src/main/java/com/example/api/domain/ChatRoom.java index 4760b7c5..9f063c48 100644 --- a/src/main/java/com/example/api/domain/ChatRoom.java +++ b/src/main/java/com/example/api/domain/ChatRoom.java @@ -3,7 +3,6 @@ import jakarta.persistence.*; import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.Setter; import java.time.LocalDateTime; @@ -11,17 +10,15 @@ @Entity @Getter -@Setter @EqualsAndHashCode @Table(name = "CHAT_ROOM") public class ChatRoom { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long chatRoomId; @OneToOne(fetch = LAZY) - @JoinColumn(name = "SUGGEST_ID") + @JoinColumn(name = "SUGGEST_ID", nullable = true, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private OfferEmployment offerEmployment; @Column(name = "SUGGEST_GENERATED_DATE") @@ -32,5 +29,8 @@ protected void onCreate() { this.suggestGeneratedDate = LocalDateTime.now(); } -} - + public ChatRoom(OfferEmployment offerEmployment) { + this.offerEmployment = offerEmployment; + this.suggestGeneratedDate = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/domain/Contract.java b/src/main/java/com/example/api/domain/Contract.java index ce288644..a75e6a6a 100644 --- a/src/main/java/com/example/api/domain/Contract.java +++ b/src/main/java/com/example/api/domain/Contract.java @@ -1,22 +1,26 @@ package com.example.api.domain; import jakarta.persistence.*; +import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import java.time.LocalDateTime; @Entity -@Getter @Setter +@Getter @Table(name = "CONTRACT") +@EqualsAndHashCode(callSuper = false) +@NoArgsConstructor public class Contract extends BaseEntity { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name ="CONTRACT_ID") private Long contractId; @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "SUGGEST_ID") + @PrimaryKeyJoinColumn(name = "CONTRACT_ID", referencedColumnName = "SUGGEST_ID", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private OfferEmployment offerEmployment; @Column(name = "CONTRACT_START_TIME") @@ -27,5 +31,38 @@ public class Contract extends BaseEntity { private int contractHourlyPay; @Column(name = "CONTRACT_SUCCEDED", columnDefinition = "boolean DEFAULT false") private boolean contractSucceeded; -} + public Contract( + final OfferEmployment offerEmployment, + final LocalDateTime contractStartTime, + final LocalDateTime contractEndTime, + final int contractHourlyPay, + final boolean contractSucceeded + ) { + this.offerEmployment = offerEmployment; + this.contractStartTime = contractStartTime; + this.contractEndTime = contractEndTime; + this.contractHourlyPay = contractHourlyPay; + this.contractSucceeded = contractSucceeded; + } + + public void updateHourlyPayment(final Integer hourlyPay) { + this.contractHourlyPay = hourlyPay; + } + + public void updateStartDateTime(final LocalDateTime contractStartTime) { + this.contractStartTime = contractStartTime; + } + + public void updateEndDateTime(final LocalDateTime contractEndTime) { + this.contractEndTime = contractEndTime; + } + + public boolean isValidContractRangeTime() { + return this.contractStartTime.isBefore(this.contractEndTime); + } + + public void succeed() { + this.contractSucceeded = true; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/domain/ExternalCareer.java b/src/main/java/com/example/api/domain/ExternalCareer.java index 547ffaef..ed7f513e 100644 --- a/src/main/java/com/example/api/domain/ExternalCareer.java +++ b/src/main/java/com/example/api/domain/ExternalCareer.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; -import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -11,7 +11,7 @@ @Entity @Getter -@Setter +@EqualsAndHashCode(callSuper = false) @Table(name = "EXTERANL_CARRER") public class ExternalCareer extends BaseEntity{ @Id @@ -19,12 +19,14 @@ public class ExternalCareer extends BaseEntity{ private Long id; @ManyToOne(fetch = LAZY) - @JoinColumn(name = "employee_id") + @JoinColumn(name = "employee_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) @JsonIgnore private Account employee; + @EqualsAndHashCode.Include @Column(name = "BUSINESS_NAME") private String Name; + @EqualsAndHashCode.Include @Column(name = "PART_TIME_PERIOD") private String period; @@ -35,7 +37,5 @@ public ExternalCareer(Account employee, String name, String period) { } public ExternalCareer() { - } -} - +} \ No newline at end of file diff --git a/src/main/java/com/example/api/domain/Flavored.java b/src/main/java/com/example/api/domain/Flavored.java index 9311f014..83db3fa2 100644 --- a/src/main/java/com/example/api/domain/Flavored.java +++ b/src/main/java/com/example/api/domain/Flavored.java @@ -1,6 +1,7 @@ package com.example.api.domain; import jakarta.persistence.*; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -8,9 +9,9 @@ @Entity @Getter -@Setter +@EqualsAndHashCode(callSuper = false) @Table(name = "flavored", uniqueConstraints = { - @UniqueConstraint(columnNames = {"account_id", "category_id"}) + @UniqueConstraint(columnNames = {"employee_id", "category_id"}) }) public class Flavored extends BaseEntity{ @Id @@ -19,11 +20,18 @@ public class Flavored extends BaseEntity{ private Long flavoredId; @ManyToOne(fetch = LAZY) - @JoinColumn(name = "category_id") + @JoinColumn(name = "category_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private Category category; @ManyToOne(fetch = LAZY) - @JoinColumn(name = "employee_id") + @JoinColumn(name = "employee_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private Account employee; -} + public Flavored() { + } + + public Flavored(Category category, Account employee) { + this.category = category; + this.employee = employee; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/domain/Inquiry.java b/src/main/java/com/example/api/domain/Inquiry.java new file mode 100644 index 00000000..8b420d2a --- /dev/null +++ b/src/main/java/com/example/api/domain/Inquiry.java @@ -0,0 +1,48 @@ +package com.example.api.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +import static jakarta.persistence.FetchType.LAZY; + +@Entity +@Getter +@Table(name = "INQUIRY") +public class Inquiry extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long inquiryId; + + private Long createdBy; + + private String inquiryType; + + private String subInquiryType; + + private String title; + + private String content; + + @Enumerated(EnumType.STRING) + private InquiryStatus inquiryStatus; + + private LocalDateTime answerDate; + public enum InquiryStatus { + WAITING, COMPLETED + } + + public Inquiry() {} + + public Inquiry(Long createdBy, String inquiryType, String subInquiryType, String title, String content, InquiryStatus inquiryStatus, LocalDateTime answerDate) { + this.createdBy = createdBy; + this.inquiryType = inquiryType; + this.subInquiryType = subInquiryType; + this.title = title; + this.content = content; + this.inquiryStatus = inquiryStatus; + this.answerDate = answerDate; + } +} diff --git a/src/main/java/com/example/api/domain/OfferEmployment.java b/src/main/java/com/example/api/domain/OfferEmployment.java index 361810c2..87197867 100644 --- a/src/main/java/com/example/api/domain/OfferEmployment.java +++ b/src/main/java/com/example/api/domain/OfferEmployment.java @@ -1,9 +1,8 @@ package com.example.api.domain; +import com.example.api.offeremployment.dto.OfferEmploymentCommand; import jakarta.persistence.*; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; +import lombok.*; import java.time.LocalDateTime; @@ -11,37 +10,70 @@ @Entity @Getter -@Setter @EqualsAndHashCode @Table(name = "OFFER_EMPLOYMENT") +@NoArgsConstructor public class OfferEmployment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long suggestId; @OneToOne(fetch = LAZY) - @JoinColumn(name = "BUSINESS_ID") + @JoinColumn(name = "BUSINESS_ID", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private Business business; - @OneToOne(fetch = LAZY) + + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "EMPLOYEE_ID") private Account employee; + @OneToOne(mappedBy = "offerEmployment", cascade = CascadeType.ALL) private Contract contract; + @Column(name = "SUGGEST_START_TIME") private LocalDateTime suggestStartTime; + @Column(name = "SUGGEST_END_TIME") private LocalDateTime suggestEndTime; + @Column(name = "SUGGEST_HOURLY_PAY") private int suggestHourlyPay; + @Column(name = "SUGGEST_READED", columnDefinition = "boolean DEFAULT false") private boolean suggestReaded; + @Column(name = "SUGGEST_SUCCEDED", columnDefinition = "boolean DEFAULT false") private boolean suggestSucceeded; + @Column(name = "SUGGEST_REGISTER_TIME") private LocalDateTime suggestRegisterTime; + @Column(name = "SUGGEST_FINISHED", columnDefinition = "boolean DEFAULT false") + private boolean suggestFinished; + + public static OfferEmployment fromCommand(OfferEmploymentCommand offerEmploymentCommand, Account employee, Business business) { + return new OfferEmployment( + business, + employee, + offerEmploymentCommand.suggestStartTime(), + offerEmploymentCommand.suggestEndTime(), + offerEmploymentCommand.suggestHourlyPay() + ); + } + + public OfferEmployment(Business business, Account employee, LocalDateTime suggestStartTime, LocalDateTime suggestEndTime, int suggestHourlyPay) { + this.business = business; + this.employee = employee; + this.suggestStartTime = suggestStartTime; + this.suggestEndTime = suggestEndTime; + this.suggestHourlyPay = suggestHourlyPay; + } + @PrePersist protected void onCreate() { this.suggestRegisterTime = LocalDateTime.now(); } + + public void succeeded() { + this.suggestSucceeded = true; + } } diff --git a/src/main/java/com/example/api/domain/PossibleBoard.java b/src/main/java/com/example/api/domain/PossibleBoard.java index 520d0586..3684b7c9 100644 --- a/src/main/java/com/example/api/domain/PossibleBoard.java +++ b/src/main/java/com/example/api/domain/PossibleBoard.java @@ -1,10 +1,7 @@ package com.example.api.domain; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import java.time.LocalDateTime; @@ -12,8 +9,8 @@ @Entity @Getter -@Setter @AllArgsConstructor +@EqualsAndHashCode(callSuper = false) @NoArgsConstructor public class PossibleBoard extends BaseEntity{ @Id @@ -21,16 +18,18 @@ public class PossibleBoard extends BaseEntity{ private Long possibleId; @ManyToOne(fetch = LAZY) - @JoinColumn(name = "EMPLOYEE_ID") + @JoinColumn(name = "EMPLOYEE_ID", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private Account employee; @Column(name = "POSSIBLE_START_TIME") + @EqualsAndHashCode.Include private LocalDateTime startTime; @Column(name = "POSSIBLE_END_TIME") + @EqualsAndHashCode.Include private LocalDateTime endTime; - public PossibleBoard(Account employee, LocalDateTime startTime, LocalDateTime endTime) { + public PossibleBoard(final Account employee, final LocalDateTime startTime, final LocalDateTime endTime) { this.employee = employee; this.startTime = startTime; this.endTime = endTime; diff --git a/src/main/java/com/example/api/domain/Review.java b/src/main/java/com/example/api/domain/Review.java index bbdadd9f..1ff66018 100644 --- a/src/main/java/com/example/api/domain/Review.java +++ b/src/main/java/com/example/api/domain/Review.java @@ -1,27 +1,47 @@ package com.example.api.domain; import jakarta.persistence.*; -import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; @Entity @Getter -@Setter @Table(name = "REVIEW") -@AllArgsConstructor @NoArgsConstructor +@EqualsAndHashCode(callSuper = true) public class Review extends BaseEntity { - @Id + @Column(name ="REVIEW_ID") @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long suggestId; + private Long reviewId; + + @OneToOne(fetch = FetchType.LAZY) + @PrimaryKeyJoinColumn(name = "REVIEW_ID", referencedColumnName = "SUGGEST_ID", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + private OfferEmployment offerEmployment; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "REVIEW_WRITER_ID") + private Business writer; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "WORKER_ID") + private Account employee; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "CONTRACT_ID") + private Contract contract; @Column(name = "REVIEW_STAR_POINT") private int reviewStarPoint; @Column(name = "REVIEW_CONTENT") private String reviewContent; + public Review(int reviewStarPoint, String reviewContent, Contract contract) { + this.reviewStarPoint = reviewStarPoint; + this.reviewContent = reviewContent; + this.contract = contract; + } } + diff --git a/src/main/java/com/example/api/domain/ReviewReport.java b/src/main/java/com/example/api/domain/ReviewReport.java new file mode 100644 index 00000000..4fe02ebc --- /dev/null +++ b/src/main/java/com/example/api/domain/ReviewReport.java @@ -0,0 +1,34 @@ +package com.example.api.domain; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +import static jakarta.persistence.FetchType.LAZY; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "REVIEW_REPORT") +public class ReviewReport { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long reportId; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "REVIEW_ID") + private Review review; + + @Column(name = "REASON") + private String reason; + + public ReviewReport(Review review, String reason) { + this.review = review; + this.reason = reason; + } +} diff --git a/src/main/java/com/example/api/domain/Scrap.java b/src/main/java/com/example/api/domain/Scrap.java index a8d20e1a..3182db1b 100644 --- a/src/main/java/com/example/api/domain/Scrap.java +++ b/src/main/java/com/example/api/domain/Scrap.java @@ -1,6 +1,7 @@ package com.example.api.domain; import jakarta.persistence.*; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -8,7 +9,7 @@ @Entity @Getter -@Setter +@EqualsAndHashCode(callSuper = false) @Table(name = "SCRAP") public class Scrap extends BaseEntity { @Id @@ -16,9 +17,9 @@ public class Scrap extends BaseEntity { private Long scrapId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "EMPLOYER_ID") + @JoinColumn(name = "EMPLOYER_ID", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private Account employer; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "EMPLOYEE_ID") + @JoinColumn(name = "EMPLOYEE_ID", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) private Account employee; -} +} \ No newline at end of file diff --git a/src/main/java/com/example/api/domain/repository/BusinessCategoryRepository.java b/src/main/java/com/example/api/domain/repository/BusinessCategoryRepository.java new file mode 100644 index 00000000..51cf31d8 --- /dev/null +++ b/src/main/java/com/example/api/domain/repository/BusinessCategoryRepository.java @@ -0,0 +1,9 @@ +package com.example.api.domain.repository; + +import com.example.api.domain.BusinessCategory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface BusinessCategoryRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/api/domain/repository/CategoryRepository.java b/src/main/java/com/example/api/domain/repository/CategoryRepository.java new file mode 100644 index 00000000..128775ff --- /dev/null +++ b/src/main/java/com/example/api/domain/repository/CategoryRepository.java @@ -0,0 +1,10 @@ +package com.example.api.domain.repository; + +import com.example.api.domain.Category; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + + +@Repository +public interface CategoryRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/api/domain/repository/EmployeeRepository.java b/src/main/java/com/example/api/domain/repository/EmployeeRepository.java new file mode 100644 index 00000000..f8639357 --- /dev/null +++ b/src/main/java/com/example/api/domain/repository/EmployeeRepository.java @@ -0,0 +1,13 @@ +package com.example.api.domain.repository; + +import com.example.api.domain.Account; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface EmployeeRepository extends JpaRepository { + Optional findByAccountId(@Param("employeeId") Long employeeId); +} diff --git a/src/main/java/com/example/api/domain/repository/EmployerRepository.java b/src/main/java/com/example/api/domain/repository/EmployerRepository.java new file mode 100644 index 00000000..08b5d3f2 --- /dev/null +++ b/src/main/java/com/example/api/domain/repository/EmployerRepository.java @@ -0,0 +1,9 @@ +package com.example.api.domain.repository; + +import com.example.api.domain.Account; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface EmployerRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/api/domain/repository/ExternalCareerRepository.java b/src/main/java/com/example/api/domain/repository/ExternalCareerRepository.java new file mode 100644 index 00000000..18449a97 --- /dev/null +++ b/src/main/java/com/example/api/domain/repository/ExternalCareerRepository.java @@ -0,0 +1,19 @@ +package com.example.api.domain.repository; + +import com.example.api.board.dto.response.ExternalCareerDTO; +import com.example.api.domain.ExternalCareer; +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; + +@Repository +public interface ExternalCareerRepository extends JpaRepository { + @Query("select new com.example.api.board.dto.response.ExternalCareerDTO(e.id, e.Name, e.period) " + + "from ExternalCareer e where e.employee.accountId = :employeeId") + List findAllDTOByEmployeeAccountId(@Param("employeeId") Long employeeId); + + List findAllByEmployeeAccountId(@Param("employeeId")Long employeeId); +} diff --git a/src/main/java/com/example/api/domain/repository/FlavoredRepository.java b/src/main/java/com/example/api/domain/repository/FlavoredRepository.java new file mode 100644 index 00000000..752fd5a4 --- /dev/null +++ b/src/main/java/com/example/api/domain/repository/FlavoredRepository.java @@ -0,0 +1,19 @@ +package com.example.api.domain.repository; + +import com.example.api.board.dto.response.CategoryDTO; +import com.example.api.domain.Flavored; +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; + +@Repository +public interface FlavoredRepository extends JpaRepository { + @Query("select distinct new com.example.api.board.dto.response.CategoryDTO(c.categoryId, c.categoryName) " + + "from Flavored f join Category c on f.category.categoryId = c.categoryId where f.employee.accountId = :employeeId") + List findAllCategoryDTOByEmployeeId(@Param("employeeId") long employeeId); + + List findAllByEmployeeAccountId(@Param("employeeId")Long employeeId); +} diff --git a/src/main/java/com/example/api/domain/repository/MyInfoRepository.java b/src/main/java/com/example/api/domain/repository/MyInfoRepository.java new file mode 100644 index 00000000..d6d9555d --- /dev/null +++ b/src/main/java/com/example/api/domain/repository/MyInfoRepository.java @@ -0,0 +1,17 @@ +package com.example.api.domain.repository; + +import com.example.api.board.dto.response.MyInfoDTO; +import com.example.api.domain.Account; +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; + +@Repository +public interface MyInfoRepository extends JpaRepository { + @Query("select new com.example.api.board.dto.response." + + "MyInfoDTO(a.name, a.nickname, a.age, a.sex, a.email, a.phoneNumber, a.starPoint, a.workCount) " + + "from Account a " + + "where a.accountId = :EmployeeId") + MyInfoDTO findMyInfoDTOById(@Param("EmployeeId") Long EmployeeId); +} diff --git a/src/main/java/com/example/api/domain/repository/OfferEmploymentRepository.java b/src/main/java/com/example/api/domain/repository/OfferEmploymentRepository.java new file mode 100644 index 00000000..99b89712 --- /dev/null +++ b/src/main/java/com/example/api/domain/repository/OfferEmploymentRepository.java @@ -0,0 +1,39 @@ +package com.example.api.domain.repository; + +import com.example.api.board.dto.response.InnerCareerDTO; +import com.example.api.domain.OfferEmployment; +import jakarta.validation.constraints.NotNull; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface OfferEmploymentRepository extends JpaRepository { + @Query("select new com.example.api.board.dto.response.InnerCareerDTO(b.businessName, c.contractStartTime, b.representationName, r) " + + "from OfferEmployment o " + + "join Contract c on o.suggestId = c.contractId " + + "join Business b on o.business.businessId = b.businessId "+ + "join Review r on o.suggestId = r.reviewId " + + "where o.employee.accountId = :employeeId") + List findAllDTOByEmployeeId(@Param("employeeId") long employeeId); + + List findAllByBusinessBusinessId(long businessId); + + @Query("select e.name, b.businessName, c.contractStartTime, c.contractEndTime " + + "from OfferEmployment oe " + + "join oe.contract c " + + "join oe.employee e " + + "join oe.business b " + + "where oe.suggestId = :OfferEmploymentId") + List findSuggestByOfferEmploymentId(@Param("OfferEmploymentId")long OfferEmploymentId); + + @Modifying + @Query("update OfferEmployment oe " + + "set oe.suggestFinished = true, oe.suggestEndTime = CURRENT_TIMESTAMP " + + "where oe.suggestFinished = :suggestId") + void updateSuggestStatusToFinishedBySuggestId(@Param("suggestId") Long suggestId); +} \ No newline at end of file diff --git a/src/main/java/com/example/api/employer/controller/EmployerController.java b/src/main/java/com/example/api/employer/controller/EmployerController.java new file mode 100644 index 00000000..5b5c2334 --- /dev/null +++ b/src/main/java/com/example/api/employer/controller/EmployerController.java @@ -0,0 +1,34 @@ +package com.example.api.employer.controller; + +import com.example.api.board.dto.request.EmployeeIdRequest; +import com.example.api.employer.controller.dto.EmployerBusinessesRequest; +import com.example.api.employer.controller.dto.LikeEmployeeDTO; +import com.example.api.employer.service.EmployerService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController("/api/v1/employees/") +@RequiredArgsConstructor +public class EmployerController { + private final EmployerService employerService; + + @GetMapping("favorites") + public ResponseEntity getLikeEmployee(@AuthenticationPrincipal final Long employerId) { + EmployeeIdRequest employeeIdRequest = new EmployeeIdRequest(employerId); + List result = employerService.getLikeEmployee(employeeIdRequest); + return ResponseEntity.ok(result); + } + + @GetMapping("businesses") + public ResponseEntity getEmployeeBusinessList(@AuthenticationPrincipal final Long employerId) { + EmployeeIdRequest employeeIdRequest = new EmployeeIdRequest(employerId); + List businesses = employerService.getEmployerBusinessList(employeeIdRequest); + return ResponseEntity.ok(businesses); + } +} diff --git a/src/main/java/com/example/api/employer/controller/dto/EmployerBusinessesRequest.java b/src/main/java/com/example/api/employer/controller/dto/EmployerBusinessesRequest.java new file mode 100644 index 00000000..975bb7f6 --- /dev/null +++ b/src/main/java/com/example/api/employer/controller/dto/EmployerBusinessesRequest.java @@ -0,0 +1,10 @@ +package com.example.api.employer.controller.dto; + +import com.example.api.account.entity.Location; +import jakarta.validation.constraints.NotNull; + +public record EmployerBusinessesRequest( + @NotNull String businessName, + @NotNull Location businessLocation +) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/employer/controller/dto/LikeEmployeeDTO.java b/src/main/java/com/example/api/employer/controller/dto/LikeEmployeeDTO.java new file mode 100644 index 00000000..8243278e --- /dev/null +++ b/src/main/java/com/example/api/employer/controller/dto/LikeEmployeeDTO.java @@ -0,0 +1,26 @@ +package com.example.api.employer.controller.dto; + +import com.example.api.board.dto.response.CategoryDTO; +import com.example.api.board.dto.response.ExternalCareerDTO; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +@EqualsAndHashCode +public class LikeEmployeeDTO { + private long employeeId; + private String name; + private String nickname; + private String sex; + private int age; + private float starPoint; + private long workCount; + private List externalCareerList; + private List flavoredCategoryList; +} diff --git a/src/main/java/com/example/api/employer/repository/ScrapRepository.java b/src/main/java/com/example/api/employer/repository/ScrapRepository.java new file mode 100644 index 00000000..ad0bc503 --- /dev/null +++ b/src/main/java/com/example/api/employer/repository/ScrapRepository.java @@ -0,0 +1,15 @@ +package com.example.api.employer.repository; + +import com.example.api.domain.Scrap; +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; + +@Repository +public interface ScrapRepository extends JpaRepository { + @Query("select s from Scrap s where s.employer.accountId = :employerId") + List findAllByEmployerId(@Param("employerId") long employerId); +} diff --git a/src/main/java/com/example/api/employer/service/EmployerService.java b/src/main/java/com/example/api/employer/service/EmployerService.java new file mode 100644 index 00000000..5549a433 --- /dev/null +++ b/src/main/java/com/example/api/employer/service/EmployerService.java @@ -0,0 +1,54 @@ +package com.example.api.employer.service; + +import com.example.api.board.dto.request.EmployeeIdRequest; +import com.example.api.business.BusinessRepository; +import com.example.api.domain.repository.EmployeeRepository; +import com.example.api.domain.repository.ExternalCareerRepository; +import com.example.api.domain.repository.FlavoredRepository; +import com.example.api.domain.Account; +import com.example.api.employer.controller.dto.EmployerBusinessesRequest; +import com.example.api.employer.controller.dto.LikeEmployeeDTO; +import com.example.api.employer.repository.ScrapRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class EmployerService { + private final ScrapRepository scrapRepository; + private final EmployeeRepository employeeRepository; + private final ExternalCareerRepository externalCareerRepository; + private final FlavoredRepository flavoredRepository; + private final BusinessRepository businessRepository; + + @Transactional(readOnly = true) + public List getLikeEmployee(final EmployeeIdRequest employeeIdRequest) { + Set employeeIds = scrapRepository.findAllByEmployerId(employeeIdRequest.employeeId()).stream() + .map(scrap -> scrap.getEmployee().getAccountId()) + .collect(Collectors.toSet()); + List likeEmployeeList = employeeRepository.findAllById(employeeIds); + return likeEmployeeList.stream().map( + employee -> new LikeEmployeeDTO( + employee.getAccountId(), + employee.getName(), + employee.getNickname(), + employee.getSex(), + employee.getAge(), + employee.getStarPoint(), + employee.getWorkCount(), + externalCareerRepository.findAllDTOByEmployeeAccountId(employee.getAccountId()), + flavoredRepository.findAllCategoryDTOByEmployeeId(employee.getAccountId()) + ) + ).collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public List getEmployerBusinessList(final EmployeeIdRequest employeeIdRequest) { + return businessRepository.findBusinessesByEmployeeId(employeeIdRequest.employeeId()); + } +} diff --git a/src/main/java/com/example/api/exception/ApiExceptionHandler.java b/src/main/java/com/example/api/exception/ApiExceptionHandler.java new file mode 100644 index 00000000..e9c30a83 --- /dev/null +++ b/src/main/java/com/example/api/exception/ApiExceptionHandler.java @@ -0,0 +1,21 @@ +package com.example.api.exception; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class ApiExceptionHandler { + @ExceptionHandler(BusinessException.class) + public ResponseEntity businessExceptionHandler(final BusinessException businessException) { + final ErrorCode errorCode = businessException.getErrorCode(); + final String details = businessException.getMessage(); + + return createExceptionResponse(errorCode, ExceptionResponseBody.of(errorCode, details)); + } + + private ResponseEntity createExceptionResponse(ErrorCode errorCode, ExceptionResponseBody exceptionResponseBody) { + return ResponseEntity.status(errorCode.getHttpStatus()) + .body(exceptionResponseBody); + } +} diff --git a/src/main/java/com/example/api/exception/BusinessException.java b/src/main/java/com/example/api/exception/BusinessException.java new file mode 100644 index 00000000..c765788e --- /dev/null +++ b/src/main/java/com/example/api/exception/BusinessException.java @@ -0,0 +1,18 @@ +package com.example.api.exception; + +public class BusinessException extends RuntimeException{ + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + this.errorCode = errorCode; + } + + public BusinessException(final String detailMessage, final ErrorCode errorCode) { + super(detailMessage); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/com/example/api/exception/ErrorCode.java b/src/main/java/com/example/api/exception/ErrorCode.java new file mode 100644 index 00000000..68878d72 --- /dev/null +++ b/src/main/java/com/example/api/exception/ErrorCode.java @@ -0,0 +1,54 @@ +package com.example.api.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum ErrorCode { + SERVER_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "-000", "서버 에러"), + DUPLICATE_LOGIN_ID(HttpStatus.BAD_REQUEST, "-100", "중복된 ID입니다."), + DUPLICATE_EMAIL(HttpStatus.BAD_REQUEST, "-101", "이미 가입된 이메일입니다."), + INCORRECT_CODE(HttpStatus.BAD_REQUEST, "-103", "이메일 인증에 실패하였습니다."), + EXPIRATION_DATE_END(HttpStatus.BAD_REQUEST, "-104", "코드의 유효 기간이 만료 되었습니다."), + NULL_USER(HttpStatus.BAD_REQUEST, "-105", "존재하지 않는 회원입니다."), + DELETED_USER(HttpStatus.BAD_REQUEST, "-106", "탈퇴한 회원입니다."), + INCORRECT_PASSWORD(HttpStatus.BAD_REQUEST, "-107", "틀린 비밀번호입니다."), + INCORRECT_DATA(HttpStatus.BAD_REQUEST, "-108", "올바르지 않은 정보입니다."), + INVALID_REDIRECT_URI(HttpStatus.BAD_REQUEST,"-109","유효하지 않은 REDIRECT URI입니다."), + INVALID_BUSINESS_NUMBER(HttpStatus.BAD_REQUEST,"-110","사업자 등록 정보를 확인할 수 없습니다."), + + INVALID_TOKEN(HttpStatus.BAD_REQUEST, "-T1", "올바르지 않은 AccessToken입니다."), + EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "-T2", "만료된 AccessToken입니다."), + EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "T3", "만료된 ReFreshToken"), + NULL_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED,"T4", "존재하지 않은 ReFreshToken 접근"), + NOT_ACCESS_TOKEN_FOR_REISSUE(HttpStatus.BAD_REQUEST,"T5","재발급하기에는 유효기간이 남은 AccessToken"), + TOKEN_MISSING_AUTHORITY(HttpStatus.BAD_REQUEST,"T6","권한 정보가 담겨있지 않은 토큰입니다."), + + FAIL_GENERATE_CODE(HttpStatus.INTERNAL_SERVER_ERROR, "-500", "코드 생성에 실패하였습니다."), + FAIL_SEND_EMAIL(HttpStatus.INTERNAL_SERVER_ERROR, "-501", "이메일 전송에 실패하였습니다."), + FAIL_SAVE_CODE(HttpStatus.INTERNAL_SERVER_ERROR, "-502", "코드 저장에 실패하였습니다."), + + + ACCOUNT_NOT_FOUND_EXCEPTION(HttpStatus.BAD_REQUEST, "-101", "찾을 수 없는 계정"), + + POSSIBLE_TIME_REGISTER_EXCEPTION(HttpStatus.BAD_REQUEST, "-401", "알바 가능 시간 등록 에러"), + + CATEGORY_EXCEPTION(HttpStatus.BAD_REQUEST, "-601", "카테고리 에러"), + + + BUSINESS_DOMAIN_EXCEPTION(HttpStatus.BAD_REQUEST, "-700", "비즈니스 도메인 에러"), + + CONTRACT_EXCEPTION(HttpStatus.BAD_REQUEST, "-800", "계약 도메인 에러"), + + FILE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR,"F500", "이미지 업로드 실패"); + + private final HttpStatus httpStatus; + private final String errorCode; + private final String errorDescription; + + ErrorCode(HttpStatus httpStatus, String errorCodeResponse, String errorDescription) { + this.httpStatus = httpStatus; + this.errorCode = errorCodeResponse; + this.errorDescription = errorDescription; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/exception/ExceptionResponseBody.java b/src/main/java/com/example/api/exception/ExceptionResponseBody.java new file mode 100644 index 00000000..ae905606 --- /dev/null +++ b/src/main/java/com/example/api/exception/ExceptionResponseBody.java @@ -0,0 +1,41 @@ +package com.example.api.exception; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.util.List; +import lombok.Getter; + +@Getter +@JsonPropertyOrder(value = {"errorCode"}) +public class ExceptionResponseBody { + private final String errorCode; + private final String errorDescription; + @JsonProperty("details") + private final String detailMessages; + @JsonInclude(JsonInclude.Include.NON_NULL) + private final List errors; + + private ExceptionResponseBody(String errorCode, String errorDescription, String detailMessages, List errors) { + this.errorCode = errorCode; + this.errorDescription = errorDescription; + this.detailMessages = detailMessages; + this.errors = errors; + } + + public static ExceptionResponseBody of(ErrorCode errorCode) { + return new ExceptionResponseBody(errorCode.getErrorCode(), errorCode.getErrorDescription(), null, null); + } + + public static ExceptionResponseBody of(ErrorCode errorCode, String detailMessages) { + return new ExceptionResponseBody(errorCode.getErrorCode(), errorCode.getErrorDescription(), detailMessages, null); + } + + public static ExceptionResponseBody of(ErrorCode errorCode, List errors) { + return new ExceptionResponseBody(errorCode.getErrorCode(), errorCode.getErrorDescription(), null, errors); + } + + public static ExceptionResponseBody of(ErrorCode errorCode, String detailMessages, List errors) { + return new ExceptionResponseBody(errorCode.getErrorCode(), errorCode.getErrorDescription(), detailMessages, errors); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/global/config/AmazonConfig.java b/src/main/java/com/example/api/global/config/AmazonConfig.java new file mode 100644 index 00000000..4d5a5a4c --- /dev/null +++ b/src/main/java/com/example/api/global/config/AmazonConfig.java @@ -0,0 +1,46 @@ +package com.example.api.global.config; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Getter +public class AmazonConfig { + private AWSCredentials awsCredentials; + @Value("${cloud.aws.s3.bucket}") + private String bucket; + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + @Value("${cloud.aws.region.static}") + private String region; + + @PostConstruct + public void init() { + this.awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + } + + @Bean + public AmazonS3 amazonS3() { + AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } + + @Bean + public AWSCredentialsProvider awsCredentialsProvider() { + return new AWSStaticCredentialsProvider(awsCredentials); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/global/config/EmailConfig.java b/src/main/java/com/example/api/global/config/EmailConfig.java new file mode 100644 index 00000000..411e001a --- /dev/null +++ b/src/main/java/com/example/api/global/config/EmailConfig.java @@ -0,0 +1,46 @@ +package com.example.api.global.config; + +import com.example.api.global.properties.EmailProperties; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +public class EmailConfig { + private final EmailProperties emailProperties; + + public EmailConfig(EmailProperties emailProperties) { + this.emailProperties = emailProperties; + } + + @Bean + public JavaMailSender javaMailSender() { + System.out.println("Configuring JavaMailSender with host: " + emailProperties.getHost()); + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(emailProperties.getHost()); + mailSender.setPort(emailProperties.getPort()); + mailSender.setUsername(emailProperties.getUsername()); + mailSender.setPassword(emailProperties.getPassword()); + mailSender.setDefaultEncoding("UTF-8"); + mailSender.setJavaMailProperties(getMailProperties()); + + return mailSender; + } + + private Properties getMailProperties() { + Properties properties = new Properties(); + properties.put("mail.smtp.auth", emailProperties.isAuth()); + properties.put("mail.smtp.starttls.enable", emailProperties.isStarttlsEnable()); + properties.put("mail.smtp.starttls.required", emailProperties.isStarttlsRequired()); + properties.put("mail.smtp.connectiontimeout", emailProperties.getConnectionTimeout()); + properties.put("mail.smtp.timeout", emailProperties.getTimeout()); + properties.put("mail.smtp.writetimeout", emailProperties.getWriteTimeout()); + properties.put("mail.smtp.expirationMillis", emailProperties.getExpirationMillis()); + + return properties; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/global/config/RestTemplateConfig.java b/src/main/java/com/example/api/global/config/RestTemplateConfig.java new file mode 100644 index 00000000..708cbf0e --- /dev/null +++ b/src/main/java/com/example/api/global/config/RestTemplateConfig.java @@ -0,0 +1,23 @@ +package com.example.api.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + @Bean + public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) { + return new RestTemplate(clientHttpRequestFactory); + } + + @Bean + public ClientHttpRequestFactory clientHttpRequestFactory() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(5000); // 연결 타임아웃 5초 + factory.setReadTimeout(5000); // 읽기 타임아웃 5초 + return factory; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/global/config/SecurityConfig.java b/src/main/java/com/example/api/global/config/SecurityConfig.java new file mode 100644 index 00000000..34fb79b5 --- /dev/null +++ b/src/main/java/com/example/api/global/config/SecurityConfig.java @@ -0,0 +1,109 @@ +package com.example.api.global.config; + +import com.example.api.auth.entitiy.JwtAuthenticationProvider; +import com.example.api.global.config.filter.JwtAuthenticationFilter; +import com.example.api.oauth2.entity.HttpCookieOAuth2AuthorizationRequestRepository; +import com.example.api.oauth2.entity.handler.OAuth2AuthenticationFailureHandler; +import com.example.api.oauth2.entity.handler.OAuth2AuthenticationSuccessHandler; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + + +import java.io.IOException; +import java.util.List; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + private final JwtAuthenticationProvider jwtAuthenticationProvider; + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final DefaultOAuth2UserService oauth2Service; + private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; + private final OAuth2AuthenticationSuccessHandler oauth2AuthorizationSuccessHandler; + private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .cors(cors -> cors + .configurationSource(corsConfigurationSource())) + .csrf(csrf -> csrf.disable()) + .httpBasic(httpBasic -> httpBasic.disable()) + .formLogin(formLogin -> formLogin.disable()) + .exceptionHandling(exceptionHandling -> + exceptionHandling.authenticationEntryPoint(new FailedAuthenticationEntryPoint())) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/**", "/error", "/favicon.ico", "/**/*.png", "/**/*.gif","/**/*.webp", "/**/*.svg", "/**/*.jpg", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() + .requestMatchers("/api/auth/**", "/oauth2/**", "/swagger-ui/**", "/v3/api-docs/**", "/api/v1/**", "/aws").permitAll() + .anyRequest().authenticated() + ) + .oauth2Login(oauth2 -> oauth2 + .authorizationEndpoint(authorizationEndpoint -> + authorizationEndpoint + .baseUri("/oauth2/authorization") + .authorizationRequestRepository(httpCookieOAuth2AuthorizationRequestRepository)) + .redirectionEndpoint(redirectEndpoint -> redirectEndpoint + .baseUri("/oauth2/callback/*")) + .userInfoEndpoint(endpoint -> endpoint.userService(oauth2Service)) + .successHandler(oauth2AuthorizationSuccessHandler) + .failureHandler(oAuth2AuthenticationFailureHandler) + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.addAllowedOrigin("*"); + corsConfiguration.addAllowedMethod("*"); + corsConfiguration.addAllowedHeader("*"); + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + return source; + } + + static class FailedAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException { + response.setContentType("application/json"); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + + response.getWriter().write("{\"code\": \"NP\", \"message\": \"No Permission.\"}"); + } + } + + @Bean + public AuthenticationManager authenticationManager() { + return new ProviderManager(List.of(jwtAuthenticationProvider)); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(12); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/global/config/SwaggerConfig.java b/src/main/java/com/example/api/global/config/SwaggerConfig.java new file mode 100644 index 00000000..4b71a6dd --- /dev/null +++ b/src/main/java/com/example/api/global/config/SwaggerConfig.java @@ -0,0 +1,39 @@ +package com.example.api.global.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI DanpatAPI() { + Info info = new Info() + .title("Spring API") + .description("단팥 백엔드 API 명세서입니다..") + .version("1.0.0"); + + String jwtSchemeName = "JWT TOKEN"; + // API 요청헤더에 인증정보 포함 + SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtSchemeName); + // SecuritySchemes 등록 + Components components = new Components() + .addSecuritySchemes(jwtSchemeName, new SecurityScheme() + .name(jwtSchemeName) + .type(SecurityScheme.Type.HTTP) // HTTP 방식 + .scheme("bearer") + .bearerFormat("JWT")); + + return new OpenAPI() + .addServersItem(new Server().url("/")) + .info(info) + .addSecurityItem(securityRequirement) + .components(components); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/global/config/WebConfig.java b/src/main/java/com/example/api/global/config/WebConfig.java new file mode 100644 index 00000000..4964abed --- /dev/null +++ b/src/main/java/com/example/api/global/config/WebConfig.java @@ -0,0 +1,15 @@ +package com.example.api.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.multipart.MultipartResolver; +import org.springframework.web.multipart.support.StandardServletMultipartResolver; + +@Configuration +public class WebConfig { + + @Bean + public MultipartResolver multipartResolver() { + return new StandardServletMultipartResolver(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/global/config/WebSocketConfig.java b/src/main/java/com/example/api/global/config/WebSocketConfig.java new file mode 100644 index 00000000..c10411ac --- /dev/null +++ b/src/main/java/com/example/api/global/config/WebSocketConfig.java @@ -0,0 +1,25 @@ +package com.example.api.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker("/room"); + config.setApplicationDestinationPrefixes("/chat"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws") + .setAllowedOriginPatterns("*") + .withSockJS(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/global/config/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/api/global/config/filter/JwtAuthenticationFilter.java new file mode 100644 index 00000000..2745a4c7 --- /dev/null +++ b/src/main/java/com/example/api/global/config/filter/JwtAuthenticationFilter.java @@ -0,0 +1,58 @@ +package com.example.api.global.config.filter; + +import com.example.api.auth.entitiy.JwtAuthenticationProvider; +import com.example.api.auth.entitiy.JwtAuthenticationToken; +import com.example.api.auth.service.JwtTokenProvider; +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +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.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtAuthenticationProvider jwtAuthenticationProvider; + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + try { + String accessToken = jwtTokenProvider.extractAccessToken(request); + if (accessToken != null) { + if (!jwtTokenProvider.isNotExpiredToken(accessToken)) { + throw new BusinessException(ErrorCode.EXPIRED_ACCESS_TOKEN.getErrorDescription(), ErrorCode.EXPIRED_ACCESS_TOKEN); + } + + Authentication authentication = jwtAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessToken)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } catch (BusinessException ex) { + response.setStatus(ex.getErrorCode().getHttpStatus().value()); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + Map errorResponse = Map.of( + "httpStatus", ex.getErrorCode().getHttpStatus().value(), + "errorCodeResponse", ex.getErrorCode().getErrorCode(), + "errorMessage", ex.getErrorCode().getErrorDescription() + ); + + response.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse)); + } finally { + SecurityContextHolder.clearContext(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/global/config/resolver/ValidEmail.java b/src/main/java/com/example/api/global/config/resolver/ValidEmail.java new file mode 100644 index 00000000..3d354e80 --- /dev/null +++ b/src/main/java/com/example/api/global/config/resolver/ValidEmail.java @@ -0,0 +1,17 @@ +package com.example.api.global.config.resolver; + +import com.example.api.global.validator.ValidEmailValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = { ValidEmailValidator.class }) +@Documented +public @interface ValidEmail { + String message() default "이메일이 유효하지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} \ No newline at end of file diff --git a/src/main/java/com/example/api/global/properties/EmailProperties.java b/src/main/java/com/example/api/global/properties/EmailProperties.java new file mode 100644 index 00000000..5b1d8415 --- /dev/null +++ b/src/main/java/com/example/api/global/properties/EmailProperties.java @@ -0,0 +1,45 @@ +package com.example.api.global.properties; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Getter +@Component +public class EmailProperties { + @Value("${spring.mail.host}") + private String host; + + @Value("${spring.mail.port}") + private int port; + + @Value("${spring.mail.username}") + private String username; + + @Value("${spring.mail.password}") + private String password; + + @Value("${spring.mail.protocol}") + private String protocol; + + @Value("${spring.mail.properties.mail.smtp.auth}") + private boolean auth; + + @Value("${spring.mail.properties.mail.smtp.starttls.enable}") + private boolean starttlsEnable; + + @Value("${spring.mail.properties.mail.smtp.starttls.required}") + private boolean starttlsRequired; + + @Value("${spring.mail.properties.mail.smtp.connectiontimeout}") + private int connectionTimeout; + + @Value("${spring.mail.properties.mail.smtp.timeout}") + private int timeout; + + @Value("${spring.mail.properties.mail.smtp.writetimeout}") + private int writeTimeout; + + @Value("${spring.mail.auth-code-expiration-millis}") + private int expirationMillis; +} \ No newline at end of file diff --git a/src/main/java/com/example/api/global/properties/JwtProperties.java b/src/main/java/com/example/api/global/properties/JwtProperties.java new file mode 100644 index 00000000..f0f15df6 --- /dev/null +++ b/src/main/java/com/example/api/global/properties/JwtProperties.java @@ -0,0 +1,33 @@ +package com.example.api.global.properties; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +@Getter +@Component +public class JwtProperties { + @Value("${jwt.secret_key}") + private String secretKey; + + @Value("${jwt.access_token_valid_time}") + private Long accessTokenValidTime; + + @Value("${jwt.refresh_token_valid_time}") + private Long refreshTokenValidTime ; + + + public byte[] getBytesSecretKey() { + return secretKey.getBytes(StandardCharsets.UTF_8); + } + + public void setAccessTokenValidTime(final Long time) { + accessTokenValidTime = time; + } + + public void setRefreshTokenValidTime(final Long time) { + refreshTokenValidTime = time; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/global/properties/Oauth2Properties.java b/src/main/java/com/example/api/global/properties/Oauth2Properties.java new file mode 100644 index 00000000..e5ece356 --- /dev/null +++ b/src/main/java/com/example/api/global/properties/Oauth2Properties.java @@ -0,0 +1,53 @@ +package com.example.api.global.properties; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Getter +public class Oauth2Properties { + @Value("${spring.security.oauth2.client.registration.kakao.client-id}") + private String kakao_clientId; + @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}") + private String kakao_redirectUri; + @Value("${spring.security.oauth2.client.registration.kakao.client-secret}") + private String kakao_clientSecret; + @Value("${spring.security.oauth2.client.registration.kakao.authorization-grant-type}") + private String kakao_authorizationGrantType; + @Value("${spring.security.oauth2.client.registration.kakao.client-authentication-method}") + private String kakao_clientAuthenticationMethod; + @Value("#{'${spring.security.oauth2.client.registration.kakao.scope}'.split(',')}") + List kakao_scopes; + @Value("${spring.security.oauth2.client.provider.kakao.authorization-uri}") + private String kakao_authorizationUri; + @Value("${spring.security.oauth2.client.provider.kakao.token-uri}") + private String kakao_tokenUri; + @Value("${spring.security.oauth2.client.provider.kakao.user-info-uri}") + private String kakao_userInfoUri; + @Value("${spring.security.oauth2.client.provider.kakao.user-name-attribute}") + private String kakao_userNameAttribute; + + @Value("${spring.security.oauth2.client.registration.naver.client-id}") + private String naver_clientId; + @Value("${spring.security.oauth2.client.registration.naver.redirect-uri}") + private String naver_redirectUri; + @Value("${spring.security.oauth2.client.registration.naver.client-secret}") + private String naver_clientSecret; + @Value("${spring.security.oauth2.client.registration.naver.authorization-grant-type}") + private String naver_authorizationGrantType; + @Value("${spring.security.oauth2.client.registration.naver.client-authentication-method}") + private String naver_clientAuthenticationMethod; + @Value("#{'${spring.security.oauth2.client.registration.naver.scope}'.split(',')}") + List naver_scopes; + @Value("${spring.security.oauth2.client.provider.naver.authorization-uri}") + private String naver_authorizationUri; + @Value("${spring.security.oauth2.client.provider.naver.token-uri}") + private String naver_tokenUri; + @Value("${spring.security.oauth2.client.provider.naver.user-info-uri}") + private String naver_userInfoUri; + @Value("${spring.security.oauth2.client.provider.naver.user-name-attribute}") + private String naver_userNameAttribute; +} \ No newline at end of file diff --git a/src/main/java/com/example/api/global/properties/VendorProperties.java b/src/main/java/com/example/api/global/properties/VendorProperties.java new file mode 100644 index 00000000..643c0fba --- /dev/null +++ b/src/main/java/com/example/api/global/properties/VendorProperties.java @@ -0,0 +1,14 @@ +package com.example.api.global.properties; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Getter +@Component +public class VendorProperties { + @Value("${vendor.api.base-url}") + private String baseUrl; + @Value("${vendor.api.service-key}") + private String serviceKey; +} \ No newline at end of file diff --git a/src/main/java/com/example/api/global/validator/ValidEmailValidator.java b/src/main/java/com/example/api/global/validator/ValidEmailValidator.java new file mode 100644 index 00000000..d48dd468 --- /dev/null +++ b/src/main/java/com/example/api/global/validator/ValidEmailValidator.java @@ -0,0 +1,34 @@ +package com.example.api.global.validator; + +import com.example.api.global.config.resolver.ValidEmail; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.net.UnknownHostException; +import java.util.regex.Pattern; + +public class ValidEmailValidator implements ConstraintValidator { + private static final String EMAIL_REGEX = "[a-zA-Z0-9._%+!#$&'*/=?^`{|}~]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + // 이메일이 null이거나, 공란인지 확인 + if (value == null || value.isBlank()) { + return false; + } + + // 기본 이메일 형식 확인 + if (!Pattern.matches(EMAIL_REGEX, value)) { + return false; + } + + // 도메인 유효성 확인 (간단한 예) + try { + String domain = value.substring(value.indexOf("@") + 1); + java.net.InetAddress.getByName(domain); + return true; + } catch (UnknownHostException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/inquiry/InquiryRepository.java b/src/main/java/com/example/api/inquiry/InquiryRepository.java new file mode 100644 index 00000000..51bb1646 --- /dev/null +++ b/src/main/java/com/example/api/inquiry/InquiryRepository.java @@ -0,0 +1,11 @@ +package com.example.api.inquiry; + +import com.example.api.domain.Inquiry; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface InquiryRepository extends JpaRepository { + List findByCreatedBy(Long createdBy); +} + + diff --git a/src/main/java/com/example/api/inquiry/InquiryService.java b/src/main/java/com/example/api/inquiry/InquiryService.java new file mode 100644 index 00000000..6555393b --- /dev/null +++ b/src/main/java/com/example/api/inquiry/InquiryService.java @@ -0,0 +1,64 @@ +package com.example.api.inquiry; + +import com.example.api.domain.Inquiry; +import com.example.api.inquiry.dto.InquiryCommand; +import com.example.api.inquiry.dto.InquiryRequest; +import com.example.api.inquiry.dto.InquiryResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class InquiryService { + private final InquiryRepository inquiryRepository; + + @Transactional + public InquiryResponse saveInquiry( + @Validated final InquiryRequest inquiryRequest, + @Validated final Long memberId + ) { + final InquiryCommand command = inquiryRequest.toCommand(memberId); + final Inquiry inquiry = mapToInquiry(command); + Inquiry savedInquiry = inquiryRepository.save(inquiry); + return mapToInquiryResponse(savedInquiry); + } + + @Transactional(readOnly = true) + public List getInquiriesByAccountId(@Validated final Long memberId) { + final List inquiries = inquiryRepository.findByCreatedBy(memberId); + return inquiries.stream() + .map(this::mapToInquiryResponse) + .collect(Collectors.toList()); + } + + private Inquiry mapToInquiry(@Validated final InquiryCommand command) { + return new Inquiry( + command.createdBy(), + command.inquiryType(), + command.subInquiryType(), + command.title(), + command.content(), + Inquiry.InquiryStatus.valueOf(command.inquiryStatus()), + command.answerDate() + ); + } + + private InquiryResponse mapToInquiryResponse(@Validated final Inquiry inquiry) { + return new InquiryResponse( + inquiry.getInquiryId(), + inquiry.getInquiryType(), + inquiry.getSubInquiryType(), + inquiry.getTitle(), + inquiry.getContent(), + inquiry.getInquiryStatus().name(), + inquiry.getAnswerDate(), + inquiry.getCreatedBy() + ); + } +} + diff --git a/src/main/java/com/example/api/inquiry/controller/InquiryController.java b/src/main/java/com/example/api/inquiry/controller/InquiryController.java new file mode 100644 index 00000000..3686dfdd --- /dev/null +++ b/src/main/java/com/example/api/inquiry/controller/InquiryController.java @@ -0,0 +1,54 @@ +package com.example.api.inquiry.controller; + +import com.example.api.domain.Inquiry; +import com.example.api.inquiry.InquiryService; +import com.example.api.inquiry.dto.InquiryCommand; +import com.example.api.inquiry.dto.InquiryRequest; +import com.example.api.inquiry.dto.InquiryResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/support") +public class InquiryController { + private final InquiryService inquiryService; + + @PostMapping("/inquiry") + public ResponseEntity createInquiry( + @AuthenticationPrincipal final Long memberId, + @RequestBody final InquiryRequest inquiryRequest + ) { + final InquiryResponse inquiryResponse = inquiryService.saveInquiry(inquiryRequest, memberId); + return ResponseEntity.ok(inquiryResponse); + } + + @GetMapping("/my-inquiries") + public ResponseEntity> getMyInquiries( + @AuthenticationPrincipal final Long memberId + ) { + final List inquiryResponses = inquiryService.getInquiriesByAccountId(memberId); + return ResponseEntity.ok(inquiryResponses); + } + + private InquiryResponse mapToResponse(Inquiry inquiry) { + return new InquiryResponse( + inquiry.getInquiryId(), + inquiry.getInquiryType(), + inquiry.getSubInquiryType(), + inquiry.getTitle(), + inquiry.getContent(), + inquiry.getInquiryStatus().name(), + inquiry.getAnswerDate(), + inquiry.getCreatedBy() + ); + } +} + + + diff --git a/src/main/java/com/example/api/inquiry/dto/InquiryCommand.java b/src/main/java/com/example/api/inquiry/dto/InquiryCommand.java new file mode 100644 index 00000000..abf6320b --- /dev/null +++ b/src/main/java/com/example/api/inquiry/dto/InquiryCommand.java @@ -0,0 +1,19 @@ +package com.example.api.inquiry.dto; + +import com.example.api.domain.Account; +import org.springframework.lang.NonNull; +import java.time.LocalDateTime; + +public record InquiryCommand( + @NonNull + Long inquiryId, + String inquiryType, + String subInquiryType, + String title, + String content, + String inquiryStatus, + LocalDateTime answerDate, + @NonNull + Long createdBy +) { +} diff --git a/src/main/java/com/example/api/inquiry/dto/InquiryRequest.java b/src/main/java/com/example/api/inquiry/dto/InquiryRequest.java new file mode 100644 index 00000000..7c833223 --- /dev/null +++ b/src/main/java/com/example/api/inquiry/dto/InquiryRequest.java @@ -0,0 +1,24 @@ +package com.example.api.inquiry.dto; + +import com.example.api.domain.Account; +import org.springframework.lang.NonNull; + +public record InquiryRequest( + @NonNull String inquiryType, + String subInquiryType, + String title, + String content +) { + public InquiryCommand toCommand(final Long memberId) { + return new InquiryCommand( + null, + inquiryType, + subInquiryType, + title, + content, + "WAITING", + null, + memberId + ); + } +} diff --git a/src/main/java/com/example/api/inquiry/dto/InquiryResponse.java b/src/main/java/com/example/api/inquiry/dto/InquiryResponse.java new file mode 100644 index 00000000..06d1fce9 --- /dev/null +++ b/src/main/java/com/example/api/inquiry/dto/InquiryResponse.java @@ -0,0 +1,20 @@ +package com.example.api.inquiry.dto; + +import com.example.api.domain.Account; +import org.springframework.lang.NonNull; + +import java.time.LocalDateTime; + +public record InquiryResponse( + @NonNull + Long inquiryId, + String inquiryType, + String subInquiryType, + String title, + String content, + String inquiryStatus, + LocalDateTime answerDate, + @NonNull + Long createdBy +) { +} diff --git a/src/main/java/com/example/api/oauth2/dto/KakaoResponse.java b/src/main/java/com/example/api/oauth2/dto/KakaoResponse.java new file mode 100644 index 00000000..4b05e99c --- /dev/null +++ b/src/main/java/com/example/api/oauth2/dto/KakaoResponse.java @@ -0,0 +1,30 @@ +package com.example.api.oauth2.dto; + +import jakarta.validation.constraints.NotNull; + +import java.util.Map; + +public record KakaoResponse(@NotNull Map attributes) implements OAuth2Response { + + @Override + public String getProvider() { + return "kakao"; + } + + @Override + public String getProviderId() { + return attributes.get("id").toString(); + } + + @Override + public String getEmail() { + Map kakaoAccount = (Map) attributes.get("kakao_account"); + return kakaoAccount != null ? kakaoAccount.get("email").toString() : null; + } + + @Override + public String getName() { + Map kakaoAccount = (Map) attributes.get("properties"); + return kakaoAccount != null ? kakaoAccount.get("nickname").toString() : null; + } +} diff --git a/src/main/java/com/example/api/oauth2/dto/NaverResponse.java b/src/main/java/com/example/api/oauth2/dto/NaverResponse.java new file mode 100644 index 00000000..061cd546 --- /dev/null +++ b/src/main/java/com/example/api/oauth2/dto/NaverResponse.java @@ -0,0 +1,28 @@ +package com.example.api.oauth2.dto; + +import jakarta.validation.constraints.NotNull; + +import java.util.Map; + +public record NaverResponse(@NotNull Map attributes) implements OAuth2Response { + + @Override + public String getProvider() { + return "naver"; + } + + @Override + public String getProviderId() { + return attributes.get("id").toString(); + } + + @Override + public String getEmail() { + return attributes.get("email").toString(); + } + + @Override + public String getName() { + return attributes.get("name").toString(); + } +} diff --git a/src/main/java/com/example/api/oauth2/dto/OAuth2Response.java b/src/main/java/com/example/api/oauth2/dto/OAuth2Response.java new file mode 100644 index 00000000..664f5412 --- /dev/null +++ b/src/main/java/com/example/api/oauth2/dto/OAuth2Response.java @@ -0,0 +1,8 @@ +package com.example.api.oauth2.dto; + +public interface OAuth2Response { + String getProvider(); + String getProviderId(); + String getEmail(); + String getName(); +} \ No newline at end of file diff --git a/src/main/java/com/example/api/oauth2/dto/Oauth2SignUpRequest.java b/src/main/java/com/example/api/oauth2/dto/Oauth2SignUpRequest.java new file mode 100644 index 00000000..6a44ea14 --- /dev/null +++ b/src/main/java/com/example/api/oauth2/dto/Oauth2SignUpRequest.java @@ -0,0 +1,7 @@ +package com.example.api.oauth2.dto; + +import com.example.api.global.config.resolver.ValidEmail; +import jakarta.validation.constraints.NotBlank; + +public record Oauth2SignUpRequest(@NotBlank String name, @ValidEmail String email) { +} diff --git a/src/main/java/com/example/api/oauth2/dto/Oauth2UserInfoRequest.java b/src/main/java/com/example/api/oauth2/dto/Oauth2UserInfoRequest.java new file mode 100644 index 00000000..c52a277a --- /dev/null +++ b/src/main/java/com/example/api/oauth2/dto/Oauth2UserInfoRequest.java @@ -0,0 +1,8 @@ +package com.example.api.oauth2.dto; + +import com.example.api.account.entity.UserRole; +import com.example.api.global.config.resolver.ValidEmail; +import jakarta.validation.constraints.NotBlank; + +public record Oauth2UserInfoRequest(@NotBlank String name, @ValidEmail String email, @NotBlank UserRole role) { +} diff --git a/src/main/java/com/example/api/oauth2/entity/CookieUtils.java b/src/main/java/com/example/api/oauth2/entity/CookieUtils.java new file mode 100644 index 00000000..3dc0fb37 --- /dev/null +++ b/src/main/java/com/example/api/oauth2/entity/CookieUtils.java @@ -0,0 +1,57 @@ +package com.example.api.oauth2.entity; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.SerializationUtils; + +import java.util.Base64; +import java.util.Optional; + +public class CookieUtils { + public static Optional getCookie(final HttpServletRequest request, final String name) { + Cookie[] cookies = request.getCookies(); + + if (cookies != null && cookies.length > 0) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(name)) { + return Optional.of(cookie); + } + } + } + + return Optional.empty(); + } + + public static void addCookie(final HttpServletResponse response, final String name, final String value, final int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(maxAge); + response.addCookie(cookie); + } + + public static void deleteCookie(final HttpServletRequest request, final HttpServletResponse response, final String name) { + Cookie[] cookies = request.getCookies(); + if (cookies != null && cookies.length > 0) { + for (Cookie cookie: cookies) { + if (cookie.getName().equals(name)) { + cookie.setValue(""); + cookie.setPath("/"); + cookie.setMaxAge(0); + response.addCookie(cookie); + } + } + } + } + + public static String serialize(final Object object) { + return Base64.getUrlEncoder() + .encodeToString(SerializationUtils.serialize(object)); + } + + public static T deserialize(final Cookie cookie, final Class cls) { + return cls.cast(SerializationUtils.deserialize( + Base64.getUrlDecoder().decode(cookie.getValue()))); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/oauth2/entity/HttpCookieOAuth2AuthorizationRequestRepository.java b/src/main/java/com/example/api/oauth2/entity/HttpCookieOAuth2AuthorizationRequestRepository.java new file mode 100644 index 00000000..df8c8272 --- /dev/null +++ b/src/main/java/com/example/api/oauth2/entity/HttpCookieOAuth2AuthorizationRequestRepository.java @@ -0,0 +1,47 @@ +package com.example.api.oauth2.entity; + +import io.micrometer.common.util.StringUtils; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.stereotype.Component; + +@Component +public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository { + public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request"; + public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri"; + private static final int cookieExpireSeconds = 180; + + @Override + public OAuth2AuthorizationRequest loadAuthorizationRequest(final HttpServletRequest request) { + return CookieUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME) + .map(cookie -> CookieUtils.deserialize(cookie, OAuth2AuthorizationRequest.class)) + .orElse(null); + } + + @Override + public void saveAuthorizationRequest(final OAuth2AuthorizationRequest authorizationRequest, final HttpServletRequest request, final HttpServletResponse response) { + if (authorizationRequest == null) { + CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); + CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); + return; + } + + CookieUtils.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(authorizationRequest), cookieExpireSeconds); + String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME); + if (StringUtils.isNotBlank(redirectUriAfterLogin)) { + CookieUtils.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, cookieExpireSeconds); + } + } + + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest(final HttpServletRequest request ,final HttpServletResponse response) { + return this.loadAuthorizationRequest(request); + } + + public void removeAuthorizationRequestCookies(final HttpServletRequest request, final HttpServletResponse response) { + CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); + CookieUtils.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/oauth2/entity/handler/KakaoResponseHandler.java b/src/main/java/com/example/api/oauth2/entity/handler/KakaoResponseHandler.java new file mode 100644 index 00000000..09919962 --- /dev/null +++ b/src/main/java/com/example/api/oauth2/entity/handler/KakaoResponseHandler.java @@ -0,0 +1,26 @@ +package com.example.api.oauth2.entity.handler; + +import com.example.api.oauth2.dto.KakaoResponse; +import com.example.api.oauth2.dto.OAuth2Response; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.logging.Logger; + +@Component +@Slf4j +public class KakaoResponseHandler implements OAuth2ResponseHandler { + @Override + public boolean supports(final String registrationId) { + return "kakao".equalsIgnoreCase(registrationId); + } + + @Override + public OAuth2Response createResponse(final Map attributes) { + log.info("Attributes: {}", attributes); + return new KakaoResponse(attributes); + } +} + diff --git a/src/main/java/com/example/api/oauth2/entity/handler/NaverResponseHandler.java b/src/main/java/com/example/api/oauth2/entity/handler/NaverResponseHandler.java new file mode 100644 index 00000000..8f216501 --- /dev/null +++ b/src/main/java/com/example/api/oauth2/entity/handler/NaverResponseHandler.java @@ -0,0 +1,22 @@ +package com.example.api.oauth2.entity.handler; + +import com.example.api.oauth2.dto.NaverResponse; +import com.example.api.oauth2.dto.OAuth2Response; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Component +public class NaverResponseHandler implements OAuth2ResponseHandler { + + @Override + public boolean supports(final String registrationId) { + return "naver".equalsIgnoreCase(registrationId); + } + + @Override + public OAuth2Response createResponse(final Map attributes) { + return new NaverResponse((Map) attributes.get("response")); + } +} + diff --git a/src/main/java/com/example/api/oauth2/entity/handler/OAuth2AuthenticationFailureHandler.java b/src/main/java/com/example/api/oauth2/entity/handler/OAuth2AuthenticationFailureHandler.java new file mode 100644 index 00000000..02752988 --- /dev/null +++ b/src/main/java/com/example/api/oauth2/entity/handler/OAuth2AuthenticationFailureHandler.java @@ -0,0 +1,37 @@ +package com.example.api.oauth2.entity.handler; + +import com.example.api.oauth2.entity.CookieUtils; +import com.example.api.oauth2.entity.HttpCookieOAuth2AuthorizationRequestRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; + +import static com.example.api.oauth2.entity.HttpCookieOAuth2AuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME; + +@Component +@RequiredArgsConstructor +public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { + private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; + + @Override + public void onAuthenticationFailure(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException exception) throws IOException, ServletException { + String targetUrl = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME) + .map(Cookie::getValue) + .orElse(("/")); + + targetUrl = UriComponentsBuilder.fromUriString(targetUrl) + .queryParam("error", exception.getLocalizedMessage()) + .build().toUriString(); + + httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response); + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/oauth2/entity/handler/OAuth2AuthenticationSuccessHandler.java b/src/main/java/com/example/api/oauth2/entity/handler/OAuth2AuthenticationSuccessHandler.java new file mode 100644 index 00000000..f3f77d58 --- /dev/null +++ b/src/main/java/com/example/api/oauth2/entity/handler/OAuth2AuthenticationSuccessHandler.java @@ -0,0 +1,136 @@ +package com.example.api.oauth2.entity.handler; + +import com.example.api.account.entity.UserRole; +import com.example.api.account.repository.AccountRepository; +import com.example.api.auth.dto.AuthTokenRequest; +import com.example.api.auth.dto.UserDetailRequest; +import com.example.api.auth.entitiy.CustomUserDetails; +import com.example.api.auth.entitiy.RefreshToken; +import com.example.api.auth.repository.TokenRepository; +import com.example.api.auth.service.JwtTokenProvider; +import com.example.api.domain.Account; +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import com.example.api.global.properties.JwtProperties; +import com.example.api.oauth2.entity.CookieUtils; +import com.example.api.oauth2.entity.HttpCookieOAuth2AuthorizationRequestRepository; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.net.URI; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static com.example.api.oauth2.entity.HttpCookieOAuth2AuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME; + +@Component +@Slf4j +@RequiredArgsConstructor +public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + private final JwtTokenProvider tokenProvider; + private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; + private final AccountRepository accountRepository; + private final TokenRepository tokenRepository; + private final JwtProperties jwtProperties; + + @Value("app.oauth2. authorized-redirect-uris") + List authorizedRedirectUris; + + @Override + public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException, ServletException { + String targetUrl = determineTargetUrl(request, response, authentication); + + if (response.isCommitted()) { + logger.debug("Response has already been committed. Unable to redirect to " + targetUrl); + return; + } + CustomUserDetails principal = (CustomUserDetails) authentication.getPrincipal(); + UserDetailRequest userDetailRequest = new UserDetailRequest(principal.getUserId(), (Collection) authentication.getAuthorities()); + setResponse(request, response, userDetailRequest); + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } + + private void setResponse(final HttpServletRequest request, final HttpServletResponse response, final UserDetailRequest userDetailRequest) { + AuthTokenRequest authTokenRequest = generateAndSaveAuthToken(userDetailRequest); + clearAuthenticationAttributes(request, response); + + Cookie accessTokenCookie = generateAccessCookie(authTokenRequest.accessToken()); + Cookie refreshTokenCookie = generateRefreshCookie(authTokenRequest.refreshToken()); + response.addCookie(accessTokenCookie); + response.addCookie(refreshTokenCookie); + response.addCookie(new Cookie("userId", userDetailRequest.userId().toString())); + response.addCookie(new Cookie("userRole", userDetailRequest.authorities().stream().findFirst().toString())); + } + + @NotNull + private Cookie generateAccessCookie(final String accessToken) { + Cookie accessTokenCookie = new Cookie("accessToken", accessToken); + accessTokenCookie.setHttpOnly(true); + accessTokenCookie.setPath("/"); + accessTokenCookie.setMaxAge(jwtProperties.getAccessTokenValidTime().intValue()); + return accessTokenCookie; + } + + @NotNull + private Cookie generateRefreshCookie(final String refreshToken) { + Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken); + refreshTokenCookie.setHttpOnly(true); + refreshTokenCookie.setPath("/"); + refreshTokenCookie.setMaxAge(jwtProperties.getRefreshTokenValidTime().intValue()); + // https 설정 후 secure 옵션 추가 필요 + return refreshTokenCookie; + } + + @NotNull + private AuthTokenRequest generateAndSaveAuthToken(final UserDetailRequest userDetailRequest) { + Account user = accountRepository.findById(userDetailRequest.userId()).orElse(null); + + RefreshToken token = new RefreshToken(user); + String accessToken = tokenProvider.generateAccessToken(userDetailRequest); + String refreshToken = tokenProvider.generateRefreshToken(userDetailRequest, token.getId()); + + token.putRefreshToken(refreshToken); + tokenRepository.save(token); + return new AuthTokenRequest(accessToken, refreshToken); + } + + protected String determineTargetUrl(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) { + Optional redirectUri = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME) + .map(Cookie::getValue); + + if(redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) { + throw new BusinessException(ErrorCode.INVALID_REDIRECT_URI); + } + return redirectUri.orElse("http://localhost:3000"); // 로그인 성공 후 리다이렉트 url + } + + private boolean isAuthorizedRedirectUri(final String uri) { + URI clientRedirectUri = URI.create(uri); + + return authorizedRedirectUris + .stream() + .anyMatch(authorizedRedirectUri -> { + URI authorizedURI = URI.create(authorizedRedirectUri); + if(clientRedirectUri.equals(authorizedURI) || authorizedRedirectUri.equals("*")) { + return true; + } + return false; + }); + } + + protected void clearAuthenticationAttributes(final HttpServletRequest request, final HttpServletResponse response) { + super.clearAuthenticationAttributes(request); + httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/oauth2/entity/handler/OAuth2ResponseHandler.java b/src/main/java/com/example/api/oauth2/entity/handler/OAuth2ResponseHandler.java new file mode 100644 index 00000000..c01b8b20 --- /dev/null +++ b/src/main/java/com/example/api/oauth2/entity/handler/OAuth2ResponseHandler.java @@ -0,0 +1,10 @@ +package com.example.api.oauth2.entity.handler; + +import com.example.api.oauth2.dto.OAuth2Response; + +import java.util.Map; + +public interface OAuth2ResponseHandler { + boolean supports(final String registrationId); + OAuth2Response createResponse(final Map attributes); +} \ No newline at end of file diff --git a/src/main/java/com/example/api/oauth2/service/CustomOauth2UserService.java b/src/main/java/com/example/api/oauth2/service/CustomOauth2UserService.java new file mode 100644 index 00000000..08b2c24c --- /dev/null +++ b/src/main/java/com/example/api/oauth2/service/CustomOauth2UserService.java @@ -0,0 +1,64 @@ +package com.example.api.oauth2.service; + +import com.example.api.account.entity.UserRole; +import com.example.api.account.repository.AccountRepository; +import com.example.api.auth.entitiy.CustomUserDetails; +import com.example.api.domain.Account; +import com.example.api.oauth2.dto.OAuth2Response; +import com.example.api.oauth2.dto.Oauth2UserInfoRequest; +import com.example.api.oauth2.entity.handler.OAuth2ResponseHandler; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collections; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class CustomOauth2UserService extends DefaultOAuth2UserService { + private final AccountRepository accountRepository; + private final List oauth2ResponseHandlers; + + @Override + @Transactional + public OAuth2User loadUser(@Validated final OAuth2UserRequest request) throws OAuth2AuthenticationException { + OAuth2User oAuth2User = super.loadUser(request); + + String registrationId = request.getClientRegistration().getRegistrationId(); + OAuth2Response oAuth2Response = createOAuth2Response(registrationId, oAuth2User); + Oauth2UserInfoRequest userInfo = new Oauth2UserInfoRequest(oAuth2Response.getName(), oAuth2Response.getEmail(), UserRole.of(0)); + + Account user = accountRepository.findByEmail(oAuth2Response.getEmail()).orElse(null); + if(user == null) { + return saveOauth2Account(userInfo); + } + return new CustomUserDetails(user.getAccountId(), user.getName(), user.getEmail(), user.getRoles()); + } + + private OAuth2Response createOAuth2Response(final String registrationId, final OAuth2User oAuth2User) { + for (OAuth2ResponseHandler oauth2ResponseHandler : oauth2ResponseHandlers) { + if(oauth2ResponseHandler.supports(registrationId)) { + return oauth2ResponseHandler.createResponse(oAuth2User.getAttributes()); + } + } + return null; + } + + private CustomUserDetails saveOauth2Account(final Oauth2UserInfoRequest userInfo) { + Account account = new Account( + userInfo.name(), + userInfo.email(), + Collections.singletonList(userInfo.role()) + ); + Account savedUser = accountRepository.save(account); + return new CustomUserDetails(savedUser.getAccountId(), savedUser.getName(), savedUser.getEmail(), savedUser.getRoles()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/offeremployment/OfferEmploymentService.java b/src/main/java/com/example/api/offeremployment/OfferEmploymentService.java new file mode 100644 index 00000000..0d855656 --- /dev/null +++ b/src/main/java/com/example/api/offeremployment/OfferEmploymentService.java @@ -0,0 +1,48 @@ +package com.example.api.offeremployment; + +import com.example.api.account.repository.AccountRepository; +import com.example.api.business.BusinessRepository; +import com.example.api.domain.Account; +import com.example.api.domain.Business; +import com.example.api.domain.OfferEmployment; +import com.example.api.domain.repository.OfferEmploymentRepository; +import com.example.api.offeremployment.dto.*; +import com.example.api.review.ReviewRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class OfferEmploymentService { + private final OfferEmploymentRepository offerEmploymentRepository; + private final AccountRepository accountRepository; + private final BusinessRepository businessRepository; + private final ReviewRepository reviewRepository; + + @Transactional + public OfferEmploymentResponse sendOfferEmployment(final OfferEmploymentRequest offerEmploymentRequest) { + final OfferEmploymentCommand offerEmploymentCommand = offerEmploymentRequest.toCommand(); + final Account employee = accountRepository.findById(offerEmploymentCommand.employeeId()) + .orElseThrow(() -> new IllegalArgumentException("Account not found with ID: " + offerEmploymentCommand.employeeId())); + final Business business = businessRepository.findById(offerEmploymentCommand.businessId()) + .orElseThrow(() -> new IllegalArgumentException("Business not found with ID: " + offerEmploymentCommand.businessId())); + final OfferEmployment offerEmployment = OfferEmployment.fromCommand( + offerEmploymentCommand, + employee, + business + ); + final OfferEmployment savedOfferEmployment = offerEmploymentRepository.save(offerEmployment); + return OfferEmploymentResponse.fromEntity(savedOfferEmployment); + } + + @Transactional + public void completeOfferEmployment(OfferEmploymentCompleteRequest completeRequest) { + // offerEmployment를 종료로 변경, 종료 시간 업뎃 + offerEmploymentRepository.updateSuggestStatusToFinishedBySuggestId(completeRequest.suggestId()); + // 알바생 평점 조정 + Integer reviewScore = reviewRepository.findReviewStarPointBySuggestId(completeRequest.suggestId()); + // 알바 횟수 count + 1 + accountRepository.updateWorkCountAndStarPointBySuggestId(completeRequest.suggestId(), reviewScore); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/offeremployment/controller/OfferEmploymentController.java b/src/main/java/com/example/api/offeremployment/controller/OfferEmploymentController.java new file mode 100644 index 00000000..3fa02cd6 --- /dev/null +++ b/src/main/java/com/example/api/offeremployment/controller/OfferEmploymentController.java @@ -0,0 +1,37 @@ +package com.example.api.offeremployment.controller; + +import com.example.api.offeremployment.OfferEmploymentService; +import com.example.api.offeremployment.dto.OfferEmploymentCompleteRequest; +import com.example.api.offeremployment.dto.OfferEmploymentRequest; +import com.example.api.offeremployment.dto.OfferEmploymentResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/offeremployment") +@RequiredArgsConstructor +public class OfferEmploymentController { + private final OfferEmploymentService offerEmploymentService; + + @PostMapping + public ResponseEntity sendOfferEmployment( + @RequestBody final OfferEmploymentRequest offerEmploymentRequest + ) { + final OfferEmploymentResponse offerEmploymentResponse = offerEmploymentService.sendOfferEmployment(offerEmploymentRequest); + return ResponseEntity.ok(offerEmploymentResponse); + } + + @PostMapping("/complete") + public ResponseEntity completeOfferEmployment( + @RequestBody final OfferEmploymentCompleteRequest completeRequest + ) { + offerEmploymentService.completeOfferEmployment(completeRequest); + return ResponseEntity.ok("성공적으로 종료되었습니다."); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/offeremployment/dto/OfferEmploymentCommand.java b/src/main/java/com/example/api/offeremployment/dto/OfferEmploymentCommand.java new file mode 100644 index 00000000..e518f6ab --- /dev/null +++ b/src/main/java/com/example/api/offeremployment/dto/OfferEmploymentCommand.java @@ -0,0 +1,11 @@ +package com.example.api.offeremployment.dto; + +import java.time.LocalDateTime; + +public record OfferEmploymentCommand( + Long employeeId, + Long businessId, + int suggestHourlyPay, + LocalDateTime suggestStartTime, + LocalDateTime suggestEndTime +) {} diff --git a/src/main/java/com/example/api/offeremployment/dto/OfferEmploymentCompleteRequest.java b/src/main/java/com/example/api/offeremployment/dto/OfferEmploymentCompleteRequest.java new file mode 100644 index 00000000..cd96ecd2 --- /dev/null +++ b/src/main/java/com/example/api/offeremployment/dto/OfferEmploymentCompleteRequest.java @@ -0,0 +1,9 @@ +package com.example.api.offeremployment.dto; + +import jakarta.validation.constraints.NotNull; + +public record OfferEmploymentCompleteRequest( + @NotNull Long suggestId, + @NotNull Long employeeId +) { +} \ No newline at end of file diff --git a/src/main/java/com/example/api/offeremployment/dto/OfferEmploymentRequest.java b/src/main/java/com/example/api/offeremployment/dto/OfferEmploymentRequest.java new file mode 100644 index 00000000..9c36ad8a --- /dev/null +++ b/src/main/java/com/example/api/offeremployment/dto/OfferEmploymentRequest.java @@ -0,0 +1,21 @@ +package com.example.api.offeremployment.dto; + +import java.time.LocalDateTime; + +public record OfferEmploymentRequest( + Long employeeId, + Long businessId, + int suggestHourlyPay, + LocalDateTime suggestStartTime, + LocalDateTime suggestEndTime +) { + public OfferEmploymentCommand toCommand() { + return new OfferEmploymentCommand( + this.employeeId, + this.businessId, + this.suggestHourlyPay, + this.suggestStartTime, + this.suggestEndTime + ); + } +} diff --git a/src/main/java/com/example/api/offeremployment/dto/OfferEmploymentResponse.java b/src/main/java/com/example/api/offeremployment/dto/OfferEmploymentResponse.java new file mode 100644 index 00000000..6b917127 --- /dev/null +++ b/src/main/java/com/example/api/offeremployment/dto/OfferEmploymentResponse.java @@ -0,0 +1,18 @@ +package com.example.api.offeremployment.dto; + +import com.example.api.domain.OfferEmployment; + +public record OfferEmploymentResponse( + Long suggestId, + boolean success, + String message +) { + public static OfferEmploymentResponse fromEntity(OfferEmployment offerEmployment) { + return new OfferEmploymentResponse( + offerEmployment.getSuggestId(), + offerEmployment.isSuggestSucceeded(), + offerEmployment.isSuggestSucceeded() ? "Offer succeeded" : "Offer pending" + ); + } +} + diff --git a/src/main/java/com/example/api/offeremployment/dto/StarPointAndWorkCountRequest.java b/src/main/java/com/example/api/offeremployment/dto/StarPointAndWorkCountRequest.java new file mode 100644 index 00000000..ef4a670a --- /dev/null +++ b/src/main/java/com/example/api/offeremployment/dto/StarPointAndWorkCountRequest.java @@ -0,0 +1,9 @@ +package com.example.api.offeremployment.dto; + +import jakarta.validation.constraints.NotNull; + +public record StarPointAndWorkCountRequest( + @NotNull float starPoint, + @NotNull int workCount +) { +} diff --git a/src/main/java/com/example/api/possbileboard/PossibleBoardRepository.java b/src/main/java/com/example/api/possbileboard/PossibleBoardRepository.java new file mode 100644 index 00000000..18a05d2d --- /dev/null +++ b/src/main/java/com/example/api/possbileboard/PossibleBoardRepository.java @@ -0,0 +1,41 @@ +package com.example.api.possbileboard; + +import com.example.api.board.dto.response.PossibleBoardDTO; +import com.example.api.domain.Category; +import com.example.api.domain.Contract; +import com.example.api.domain.ExternalCareer; +import com.example.api.domain.PossibleBoard; +import com.example.api.possbileboard.dto.PossibleDetails; +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface PossibleBoardRepository extends JpaRepository { + @Modifying + @Query("DELETE FROM PossibleBoard possible WHERE (possible.startTime <= :endDateTime AND possible.endTime >= :startDateTime)") + Long deleteDuplicatedWorkTimeIncluded(@Param("startDateTime") final LocalDateTime startDateTimeIncluded, + @Param("endDateTime") final LocalDateTime endDateTimeIncluded); + + @Query("SELECT new com.example.api.possbileboard.dto.PossibleDetails(p.employee.name, p.employee.age, p.employee.email, p.employee.phoneNumber, p.updatedDate, p.startTime, p.endTime, COUNT (p), CAST(COALESCE(AVG(r.reviewStarPoint),0) as float)) FROM PossibleBoard p INNER JOIN Account a INNER JOIN Contract c INNER JOIN Review r WHERE p.possibleId = :possibleId GROUP BY p.employee.name, p.employee.age, p.employee.email, p.employee.phoneNumber, p.updatedDate, p.startTime, p.endTime") + PossibleDetails queryPossibleDetails(@Param("possibleId") final Long possibleId); + + @Query("SELECT f.category FROM Flavored f JOIN Account a JOIN PossibleBoard p WHERE p.possibleId = :possibleId") + List queryFlavoredCategories(@Param("possibleId") final Long possibleId); + + @Query("SELECT ex FROM ExternalCareer ex JOIN PossibleBoard p ON ex.employee.accountId = p.employee.accountId WHERE p.possibleId = :possibleId") + List queryExternalCareers(@Param("possibleId") final Long possibleId); + + @Query("SELECT c FROM Contract c JOIN PossibleBoard p ON p.employee.accountId = c.offerEmployment.employee.accountId WHERE c.offerEmployment.employee.accountId = :possibleId AND c.contractSucceeded = TRUE ") + List queryInternalCareers(@Param("possibleId") final Long possibleId); + + List findAllByEmployeeAccountId(Long employeeId); + + @Query("select new com.example.api.board.dto.response.PossibleBoardDTO(p.possibleId, p.startTime, p.endTime) " + + "from PossibleBoard p where p.employee.accountId = :employeeId") + List findAllDTOByEmployeeAccountId(@Param("employeeId")Long employeeId); +} \ No newline at end of file diff --git a/src/main/java/com/example/api/possbileboard/PossibleBoardService.java b/src/main/java/com/example/api/possbileboard/PossibleBoardService.java new file mode 100644 index 00000000..36864f10 --- /dev/null +++ b/src/main/java/com/example/api/possbileboard/PossibleBoardService.java @@ -0,0 +1,57 @@ +package com.example.api.possbileboard; + +import com.example.api.account.service.AccountService; +import com.example.api.domain.Account; +import com.example.api.domain.Category; +import com.example.api.domain.Contract; +import com.example.api.domain.ExternalCareer; +import com.example.api.domain.PossibleBoard; +import com.example.api.possbileboard.dto.AddPossibleTimeCommand; +import com.example.api.possbileboard.dto.PossibleDetails; +import com.example.api.possbileboard.dto.PossibleDetailsResponse; +import com.example.api.possbileboard.dto.QueryPossibleDetailsCommand; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PossibleBoardService { + private final PossibleBoardRepository possibleBoardRepository; + private final AccountService accountService; + private final PossibleMapper possibleMapper; + + @Transactional + public void addPossibleBoard(final AddPossibleTimeCommand addPossibleTimeCommand) { + final List possibleTimes = addPossibleTimeCommand.possibleTimes().stream() + .map(possibleTimeRange -> new PossibleTime(possibleTimeRange.startTime(), possibleTimeRange.endTime())) + .collect(Collectors.toList()); + deleteDuplicatedPeriod(possibleTimes); + final Account account = accountService.loadAccount(addPossibleTimeCommand.requestMemberId()); + addNewPeriod(account, possibleTimes); + } + + @Transactional(readOnly = true) + public PossibleDetailsResponse queryPossibleDetails(final QueryPossibleDetailsCommand queryPossibleDetailsCommand) { + final PossibleDetails possibleDetails = possibleBoardRepository.queryPossibleDetails(queryPossibleDetailsCommand.possibleId()); + final List categories = possibleBoardRepository.queryFlavoredCategories(queryPossibleDetailsCommand.possibleId()); + final List externalCareers = possibleBoardRepository.queryExternalCareers(queryPossibleDetailsCommand.possibleId()); + final List contracts = possibleBoardRepository.queryInternalCareers(queryPossibleDetailsCommand.possibleId()); + + return possibleMapper.toPossibleDetailsResponse(possibleDetails, categories, externalCareers, contracts); + } + + private void deleteDuplicatedPeriod(final List possibleTimes) { + possibleTimes.stream() + .forEach(possibleTime -> possibleBoardRepository.deleteDuplicatedWorkTimeIncluded(possibleTime.getStartTime(), possibleTime.getEndTime())); + } + + private void addNewPeriod(final Account account, final List possibleTimes) { + final List possibleBoards = possibleTimes.stream() + .map(possibleTime -> possibleMapper.toBoard(account, possibleTime)) + .toList(); + possibleBoardRepository.saveAll(possibleBoards); + } +} diff --git a/src/main/java/com/example/api/possbileboard/PossibleMapper.java b/src/main/java/com/example/api/possbileboard/PossibleMapper.java new file mode 100644 index 00000000..d3334b66 --- /dev/null +++ b/src/main/java/com/example/api/possbileboard/PossibleMapper.java @@ -0,0 +1,58 @@ +package com.example.api.possbileboard; + +import com.example.api.domain.Account; +import com.example.api.domain.Category; +import com.example.api.domain.Contract; +import com.example.api.domain.ExternalCareer; +import com.example.api.domain.PossibleBoard; +import com.example.api.possbileboard.dto.ExternalCareerResponse; +import com.example.api.possbileboard.dto.FlavoredCategory; +import com.example.api.possbileboard.dto.InternalCareerResponse; +import com.example.api.possbileboard.dto.PossibleDetails; +import com.example.api.possbileboard.dto.PossibleDetailsResponse; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; + +@Service +class PossibleMapper { + + PossibleBoard toBoard(final Account account, final PossibleTime possibleTime) { + return new PossibleBoard(account, possibleTime.getStartTime(), possibleTime.getEndTime()); + } + + PossibleDetailsResponse toPossibleDetailsResponse(final PossibleDetails possibleDetails, final List categories, final List externalCareers, final List internalCareeors) { + return new PossibleDetailsResponse( + possibleDetails.name(), + possibleDetails.age(), + possibleDetails.email(), + possibleDetails.phoneNumber(), + possibleDetails.recentlyUpdatedTime(), + possibleDetails.possibleStartTime(), + possibleDetails.possibleEndTime(), + categories.stream() + .map(this::toFlavoredCategory) + .toList(), + externalCareers.stream() + .map(this::toExternalCareerResponse) + .toList(), + internalCareeors.stream() + .map(this::toInternalCareerResponse) + .toList(), + possibleDetails.contractCount(), + possibleDetails.starPoint().intValue() + ); + } + + ExternalCareerResponse toExternalCareerResponse(final ExternalCareer externalCareer) { + return new ExternalCareerResponse(externalCareer.getId(), externalCareer.getName(), externalCareer.getPeriod()); + } + + FlavoredCategory toFlavoredCategory(final Category category) { + return new FlavoredCategory(category.getCategoryId(), category.getCategoryName()); + } + + InternalCareerResponse toInternalCareerResponse(final Contract contract) { + return new InternalCareerResponse(contract.getContractId(), contract.getContractHourlyPay(), contract.getContractStartTime(), contract.getContractEndTime()); + } +} diff --git a/src/main/java/com/example/api/possbileboard/PossibleTime.java b/src/main/java/com/example/api/possbileboard/PossibleTime.java new file mode 100644 index 00000000..c9195275 --- /dev/null +++ b/src/main/java/com/example/api/possbileboard/PossibleTime.java @@ -0,0 +1,29 @@ +package com.example.api.possbileboard; + +import com.example.api.exception.BusinessException; +import com.example.api.exception.ErrorCode; +import java.time.LocalDateTime; +import lombok.Getter; + +@Getter +class PossibleTime { + private final LocalDateTime startTime; + private final LocalDateTime endTime; + + public PossibleTime(final LocalDateTime startTime, final LocalDateTime endTime) { + validate(startTime, endTime); + this.startTime = startTime; + this.endTime = endTime; + } + + public void validate(final LocalDateTime startTime, final LocalDateTime endTime) { + validateAfterStartTime(startTime, endTime); + } + + public void validateAfterStartTime(final LocalDateTime startTime, final LocalDateTime endTime) { + if (startTime.isBefore(endTime)) { + return; + } + throw new BusinessException("종료 시간이 시작시간보다 이릅니다. 시작 시간: " + startTime + "종료 시간 : " + endTime, ErrorCode.POSSIBLE_TIME_REGISTER_EXCEPTION); + } +} diff --git a/src/main/java/com/example/api/possbileboard/controller/PossibleBoardController.java b/src/main/java/com/example/api/possbileboard/controller/PossibleBoardController.java new file mode 100644 index 00000000..4f8af6a0 --- /dev/null +++ b/src/main/java/com/example/api/possbileboard/controller/PossibleBoardController.java @@ -0,0 +1,39 @@ +package com.example.api.possbileboard.controller; + +import com.example.api.possbileboard.PossibleBoardService; +import com.example.api.possbileboard.dto.AddPossibleTimeCommand; +import com.example.api.possbileboard.dto.AddPossibleTimeRequest; +import com.example.api.possbileboard.dto.PossibleDetailsResponse; +import com.example.api.possbileboard.dto.QueryPossibleDetailsCommand; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +class PossibleBoardController { + private final PossibleBoardService possibleBoardService; + + @PostMapping("/api/v1/possible-board") + public ResponseEntity addPossibleTimes( + @RequestBody final AddPossibleTimeRequest addPossibleTimeRequest, + final Long requestMemberId + ) { + final AddPossibleTimeCommand addPossibleTimeCommand = addPossibleTimeRequest.toCommand(requestMemberId); + possibleBoardService.addPossibleBoard(addPossibleTimeCommand); + return ResponseEntity.ok().build(); + } + + @GetMapping("/api/v1/possible-board/{possibleId}") + public ResponseEntity queryPossibleBoardTimes( + @PathVariable(required = true) final Long possibleId + ) { + final QueryPossibleDetailsCommand command = new QueryPossibleDetailsCommand(possibleId); + final PossibleDetailsResponse response = possibleBoardService.queryPossibleDetails(command); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/example/api/possbileboard/dto/AddPossibleTimeCommand.java b/src/main/java/com/example/api/possbileboard/dto/AddPossibleTimeCommand.java new file mode 100644 index 00000000..7309ccfb --- /dev/null +++ b/src/main/java/com/example/api/possbileboard/dto/AddPossibleTimeCommand.java @@ -0,0 +1,15 @@ +package com.example.api.possbileboard.dto; + +import java.time.LocalDateTime; +import java.util.List; + +public record AddPossibleTimeCommand( + Long requestMemberId, + List possibleTimes +) { + public record PossibleTimeRange( + LocalDateTime startTime, + LocalDateTime endTime + ) { + } +} diff --git a/src/main/java/com/example/api/possbileboard/dto/AddPossibleTimeRequest.java b/src/main/java/com/example/api/possbileboard/dto/AddPossibleTimeRequest.java new file mode 100644 index 00000000..2e911bfe --- /dev/null +++ b/src/main/java/com/example/api/possbileboard/dto/AddPossibleTimeRequest.java @@ -0,0 +1,24 @@ +package com.example.api.possbileboard.dto; + +import com.example.api.possbileboard.dto.AddPossibleTimeCommand.PossibleTimeRange; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +public record AddPossibleTimeRequest( + List possibleTimes +) { + + public AddPossibleTimeCommand toCommand(final Long requestMemberId) { + final List possibleTimeRanges = this.possibleTimes.stream() + .map(possibleTimeForm -> new PossibleTimeRange(possibleTimeForm.startTime, possibleTimeForm.endTime)) + .collect(Collectors.toList()); + return new AddPossibleTimeCommand(requestMemberId, possibleTimeRanges); + } + + record PossibleTimeForm( + LocalDateTime startTime, + LocalDateTime endTime + ) { + } +} diff --git a/src/main/java/com/example/api/possbileboard/dto/ExternalCareerResponse.java b/src/main/java/com/example/api/possbileboard/dto/ExternalCareerResponse.java new file mode 100644 index 00000000..2420d1de --- /dev/null +++ b/src/main/java/com/example/api/possbileboard/dto/ExternalCareerResponse.java @@ -0,0 +1,8 @@ +package com.example.api.possbileboard.dto; + +public record ExternalCareerResponse( + Long externalCareerId, + String careerName, + String period +) { +} diff --git a/src/main/java/com/example/api/possbileboard/dto/FlavoredCategory.java b/src/main/java/com/example/api/possbileboard/dto/FlavoredCategory.java new file mode 100644 index 00000000..29d51cce --- /dev/null +++ b/src/main/java/com/example/api/possbileboard/dto/FlavoredCategory.java @@ -0,0 +1,7 @@ +package com.example.api.possbileboard.dto; + +public record FlavoredCategory( + Long categoryId, + String categoryName +) { +} diff --git a/src/main/java/com/example/api/possbileboard/dto/InternalCareerResponse.java b/src/main/java/com/example/api/possbileboard/dto/InternalCareerResponse.java new file mode 100644 index 00000000..42e97c44 --- /dev/null +++ b/src/main/java/com/example/api/possbileboard/dto/InternalCareerResponse.java @@ -0,0 +1,11 @@ +package com.example.api.possbileboard.dto; + +import java.time.LocalDateTime; + +public record InternalCareerResponse( + Long contractId, + Integer hourlyPayment, + LocalDateTime startTime, + LocalDateTime endTime +) { +} diff --git a/src/main/java/com/example/api/possbileboard/dto/PossibleDetails.java b/src/main/java/com/example/api/possbileboard/dto/PossibleDetails.java new file mode 100644 index 00000000..2ad8f49e --- /dev/null +++ b/src/main/java/com/example/api/possbileboard/dto/PossibleDetails.java @@ -0,0 +1,16 @@ +package com.example.api.possbileboard.dto; + +import java.time.LocalDateTime; + +public record PossibleDetails( + String name, + Integer age, + String email, + String phoneNumber, + LocalDateTime recentlyUpdatedTime, + LocalDateTime possibleStartTime, + LocalDateTime possibleEndTime, + Long contractCount, + Float starPoint +) { +} diff --git a/src/main/java/com/example/api/possbileboard/dto/PossibleDetailsResponse.java b/src/main/java/com/example/api/possbileboard/dto/PossibleDetailsResponse.java new file mode 100644 index 00000000..a909c226 --- /dev/null +++ b/src/main/java/com/example/api/possbileboard/dto/PossibleDetailsResponse.java @@ -0,0 +1,20 @@ +package com.example.api.possbileboard.dto; + +import java.time.LocalDateTime; +import java.util.List; + +public record PossibleDetailsResponse( + String name, + Integer age, + String email, + String phoneNumber, + LocalDateTime recentlyUpdatedTime, + LocalDateTime possibleStartTime, + LocalDateTime possibleEndTime, + List flavoredCategories, + List externalExperience, + List internalExperience, + Long contractCount, + Integer starPoint +) { +} diff --git a/src/main/java/com/example/api/possbileboard/dto/QueryPossibleDetailsCommand.java b/src/main/java/com/example/api/possbileboard/dto/QueryPossibleDetailsCommand.java new file mode 100644 index 00000000..4c78050a --- /dev/null +++ b/src/main/java/com/example/api/possbileboard/dto/QueryPossibleDetailsCommand.java @@ -0,0 +1,6 @@ +package com.example.api.possbileboard.dto; + +public record QueryPossibleDetailsCommand( + Long possibleId +) { +} diff --git a/src/main/java/com/example/api/review/ReviewRepository.java b/src/main/java/com/example/api/review/ReviewRepository.java new file mode 100644 index 00000000..98a21b28 --- /dev/null +++ b/src/main/java/com/example/api/review/ReviewRepository.java @@ -0,0 +1,39 @@ +package com.example.api.review; + +import com.example.api.domain.Review; +import jakarta.validation.constraints.NotNull; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ReviewRepository extends JpaRepository { + + @Query("SELECT r FROM Review r " + + "JOIN FETCH r.contract c " + + "WHERE c.offerEmployment.business.businessId = :employerId") + List loadReviewsByEmployerId(@Param("employerId") Long employerId); + + @Query("SELECT r FROM Review r " + + "WHERE (:reviewId IS NULL OR r.reviewId = :reviewId)") + List findReviewsByDynamicQuery(@Param("reviewId") Long reviewId); + + @Query("SELECT r FROM Review r " + + "JOIN FETCH r.writer b " + + "JOIN FETCH r.employee a " + + "JOIN FETCH r.contract c " + + "WHERE a.accountId = :accountId") + List findReviewsByAccountIdWithDetails(@Param("accountId") Long accountId); + + List findReviewsByEmployee_AccountId(Long accountId); + + @Query("select r.reviewStarPoint from Review r where r.reviewId = :suggestId") + Integer findReviewStarPointBySuggestId(@Param("suggestId") Long suggestId); +} + + + diff --git a/src/main/java/com/example/api/review/ReviewService.java b/src/main/java/com/example/api/review/ReviewService.java new file mode 100644 index 00000000..5ab0109c --- /dev/null +++ b/src/main/java/com/example/api/review/ReviewService.java @@ -0,0 +1,74 @@ +package com.example.api.review; + +import com.example.api.review.dto.ReviewResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import com.example.api.domain.Review; +import com.example.api.review.dto.ReviewCommand; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class ReviewService { + private final ReviewRepository reviewRepository; + + @Transactional + public List getAllReviews() { + return reviewRepository.findReviewsByDynamicQuery(null) + .stream() + .map(ReviewResponse::from) + .toList(); + } + + @Transactional + public List getReviewsByEmployee(@Validated final Long reviewId) { + return reviewRepository.findReviewsByDynamicQuery(reviewId) + .stream() + .map(ReviewResponse::from) + .toList(); + } + + @Transactional + public List getReviews(@Validated final ReviewCommand reviewCommand) { + final List reviews = reviewRepository.findReviewsByEmployee_AccountId(reviewCommand.accountId()); + return mapToReviewResponses(reviews); + } + + @Transactional + public List getReviewsByEmployeeWithDetails(@Validated final ReviewCommand reviewCommand) { + final List reviews = reviewRepository.findReviewsByAccountIdWithDetails(reviewCommand.accountId()); + return mapToReviewResponses(reviews); + } + + private List mapToReviewResponses(final List reviews) { + return reviews.stream() + .map(this::mapToReviewResponse) + .toList(); + } + + private ReviewResponse mapToReviewResponse(final Review review) { + final String businessName = review.getContract().getOfferEmployment().getBusiness().getBusinessName(); + final Long businessId = review.getContract().getOfferEmployment().getBusiness().getBusinessId(); + final LocalDateTime contractStartTime = review.getContract().getContractStartTime(); + final LocalDateTime contractEndTime = review.getContract().getContractEndTime(); + final int reviewStarPoint = review.getReviewStarPoint(); + final String reviewContent = review.getReviewContent(); + + return new ReviewResponse( + review.getReviewId(), + businessName, + businessId, + contractStartTime, + contractEndTime, + reviewStarPoint, + reviewContent + ); + } +} + diff --git a/src/main/java/com/example/api/review/controller/ReviewController.java b/src/main/java/com/example/api/review/controller/ReviewController.java new file mode 100644 index 00000000..aaaf3fd1 --- /dev/null +++ b/src/main/java/com/example/api/review/controller/ReviewController.java @@ -0,0 +1,44 @@ +package com.example.api.review.controller; + +import com.example.api.review.ReviewService; +import com.example.api.review.dto.ReviewCommand; +import com.example.api.review.dto.ReviewResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/review") +@RequiredArgsConstructor +public class ReviewController { + private final ReviewService reviewService; + + @GetMapping + public ResponseEntity> getAllReviews( + @AuthenticationPrincipal final Long memberId + ) { + final List reviews = reviewService.getAllReviews(); + return ResponseEntity.ok(reviews); + } + + @GetMapping("/{reviewId}") + public ResponseEntity> getReviewsByEmployee( + @AuthenticationPrincipal final Long memberId, + @PathVariable(required = true) final Long reivewId + ) { + final List reviews = reviewService.getReviewsByEmployee(reivewId); + return ResponseEntity.ok(reviews); + } + + @GetMapping("/my/reviews") + public ResponseEntity> getMyReviews( + @AuthenticationPrincipal final Long memberId + ) { + final ReviewCommand reviewCommand = new ReviewCommand(memberId); + final List reviews = reviewService.getReviews(reviewCommand); + return ResponseEntity.ok(reviews); + } +} diff --git a/src/main/java/com/example/api/review/dto/ReviewCommand.java b/src/main/java/com/example/api/review/dto/ReviewCommand.java new file mode 100644 index 00000000..58f47028 --- /dev/null +++ b/src/main/java/com/example/api/review/dto/ReviewCommand.java @@ -0,0 +1,7 @@ +package com.example.api.review.dto; + +public record ReviewCommand( + Long accountId +){} + + diff --git a/src/main/java/com/example/api/review/dto/ReviewResponse.java b/src/main/java/com/example/api/review/dto/ReviewResponse.java new file mode 100644 index 00000000..9ee26ef6 --- /dev/null +++ b/src/main/java/com/example/api/review/dto/ReviewResponse.java @@ -0,0 +1,28 @@ +package com.example.api.review.dto; + +import com.example.api.domain.Review; +import java.time.LocalDateTime; + +public record ReviewResponse( + Long reviewId, + String businessName, + Long businessId, + LocalDateTime contractStartTime, + LocalDateTime contractEndTime, + int reviewStarPoint, + String reviewContent +) { + public static ReviewResponse from(final Review review) { + return new ReviewResponse( + review.getReviewId(), + review.getContract().getOfferEmployment().getBusiness().getBusinessName(), + review.getContract().getOfferEmployment().getBusiness().getBusinessId(), + review.getContract().getContractStartTime(), + review.getContract().getContractEndTime(), + review.getReviewStarPoint(), + review.getReviewContent() + ); + } +} + + diff --git a/src/main/java/com/example/api/reviewavailable/ReviewAvailableService.java b/src/main/java/com/example/api/reviewavailable/ReviewAvailableService.java new file mode 100644 index 00000000..db282ca3 --- /dev/null +++ b/src/main/java/com/example/api/reviewavailable/ReviewAvailableService.java @@ -0,0 +1,22 @@ +package com.example.api.reviewavailable; + +import com.example.api.contracts.ContractRepository; +import com.example.api.reviewavailable.dto.ReviewAvailableCommand; +import com.example.api.reviewavailable.dto.ReviewAvailableResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReviewAvailableService { + private final ContractRepository contractRepository; + + @Transactional + public List getAvailableReviewTargets( + final ReviewAvailableCommand command) { + return contractRepository.findAvailableReviewsByBusinessId(command.businessId()); + } +} diff --git a/src/main/java/com/example/api/reviewavailable/controller/ReviewAvailableController.java b/src/main/java/com/example/api/reviewavailable/controller/ReviewAvailableController.java new file mode 100644 index 00000000..fd2211fe --- /dev/null +++ b/src/main/java/com/example/api/reviewavailable/controller/ReviewAvailableController.java @@ -0,0 +1,30 @@ +package com.example.api.reviewavailable.controller; + +import com.example.api.reviewavailable.ReviewAvailableService; +import com.example.api.reviewavailable.dto.ReviewAvailableCommand; +import com.example.api.reviewavailable.dto.ReviewAvailableResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/reviews") +@RequiredArgsConstructor +public class ReviewAvailableController { + private final ReviewAvailableService reviewAvailableService; + + @GetMapping("/available") + public ResponseEntity> getAvailableReviewTargets( + @RequestParam(required = true) Long businessId + ) { + ReviewAvailableCommand command = new ReviewAvailableCommand(businessId); + List availableEmployees = reviewAvailableService.getAvailableReviewTargets(command); + return ResponseEntity.ok(availableEmployees); + } +} + diff --git a/src/main/java/com/example/api/reviewavailable/dto/ReviewAvailableCommand.java b/src/main/java/com/example/api/reviewavailable/dto/ReviewAvailableCommand.java new file mode 100644 index 00000000..aff7bb82 --- /dev/null +++ b/src/main/java/com/example/api/reviewavailable/dto/ReviewAvailableCommand.java @@ -0,0 +1,5 @@ +package com.example.api.reviewavailable.dto; + +public record ReviewAvailableCommand( + Long businessId +) {} \ No newline at end of file diff --git a/src/main/java/com/example/api/reviewavailable/dto/ReviewAvailableResponse.java b/src/main/java/com/example/api/reviewavailable/dto/ReviewAvailableResponse.java new file mode 100644 index 00000000..0c477026 --- /dev/null +++ b/src/main/java/com/example/api/reviewavailable/dto/ReviewAvailableResponse.java @@ -0,0 +1,7 @@ +package com.example.api.reviewavailable.dto; + +public record ReviewAvailableResponse( + Long employeeId, + String employeeName +) {} + diff --git a/src/main/java/com/example/api/reviewreport/ReviewReportRepository.java b/src/main/java/com/example/api/reviewreport/ReviewReportRepository.java new file mode 100644 index 00000000..0b3bf33d --- /dev/null +++ b/src/main/java/com/example/api/reviewreport/ReviewReportRepository.java @@ -0,0 +1,16 @@ +package com.example.api.reviewreport; + +import com.example.api.domain.Account; +import com.example.api.domain.Review; +import com.example.api.domain.ReviewReport; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface ReviewReportRepository extends JpaRepository { + boolean existsByReview(Review review); + + Optional findByReview(Review review); +} diff --git a/src/main/java/com/example/api/reviewreport/ReviewReportService.java b/src/main/java/com/example/api/reviewreport/ReviewReportService.java new file mode 100644 index 00000000..e2dee272 --- /dev/null +++ b/src/main/java/com/example/api/reviewreport/ReviewReportService.java @@ -0,0 +1,41 @@ +package com.example.api.reviewreport; + +import com.example.api.domain.Account; +import com.example.api.domain.Review; +import com.example.api.domain.ReviewReport; +import com.example.api.reviewreport.dto.ReviewReportCommand; +import com.example.api.reviewreport.dto.ReviewReportResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ReviewReportService { + private final ReviewReportRepository reviewReportRepository; + + @Transactional + public ReviewReportResponse reportReview(final ReviewReportCommand reviewReportCommand) { + validateAlreadyReported(reviewReportCommand.review()); + final ReviewReport savedReport = saveReviewReport(reviewReportCommand); + return createResponse(savedReport); + } + + private void validateAlreadyReported(final Review review) { + boolean alreadyReported = reviewReportRepository.existsByReview(review); + if (alreadyReported) { + throw new IllegalStateException("이미 신고된 리뷰입니다."); + } + } + + private ReviewReport saveReviewReport(final ReviewReportCommand command) { + return reviewReportRepository.save(command.toEntity()); + } + + private ReviewReportResponse createResponse(ReviewReport savedReport) { + return new ReviewReportResponse( + savedReport.getReportId(), + "리뷰 신고가 성공적으로 처리되었습니다." + ); + } +} diff --git a/src/main/java/com/example/api/reviewreport/controller/ReviewReportController.java b/src/main/java/com/example/api/reviewreport/controller/ReviewReportController.java new file mode 100644 index 00000000..b45629b0 --- /dev/null +++ b/src/main/java/com/example/api/reviewreport/controller/ReviewReportController.java @@ -0,0 +1,30 @@ +package com.example.api.reviewreport.controller; + +import com.example.api.domain.Review; +import com.example.api.reviewreport.ReviewReportService; +import com.example.api.reviewreport.dto.ReviewReportCommand; +import com.example.api.reviewreport.dto.ReviewReportRequest; +import com.example.api.reviewreport.dto.ReviewReportResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/info/my/reviews") +@RequiredArgsConstructor +public class ReviewReportController { + private final ReviewReportService reviewReportService; + + @PostMapping("/{reviewId}/report") + public ResponseEntity reportReview( + @AuthenticationPrincipal final Long memberId, + @PathVariable(required = true) final Review reviewId, + @RequestBody final ReviewReportRequest reviewReportRequest + ) { + final ReviewReportCommand reviewReportCommand = reviewReportRequest.toCommand(reviewId); + final ReviewReportResponse response = reviewReportService.reportReview(reviewReportCommand); + return ResponseEntity.ok(response); + } +} + diff --git a/src/main/java/com/example/api/reviewreport/dto/ReviewReportCommand.java b/src/main/java/com/example/api/reviewreport/dto/ReviewReportCommand.java new file mode 100644 index 00000000..216aef37 --- /dev/null +++ b/src/main/java/com/example/api/reviewreport/dto/ReviewReportCommand.java @@ -0,0 +1,14 @@ +package com.example.api.reviewreport.dto; + +import com.example.api.domain.Review; +import com.example.api.domain.ReviewReport; + +public record ReviewReportCommand( + Review review, + String reason +) { + public ReviewReport toEntity() { + return new ReviewReport(review, reason); + } +} + diff --git a/src/main/java/com/example/api/reviewreport/dto/ReviewReportRequest.java b/src/main/java/com/example/api/reviewreport/dto/ReviewReportRequest.java new file mode 100644 index 00000000..3ef37a6b --- /dev/null +++ b/src/main/java/com/example/api/reviewreport/dto/ReviewReportRequest.java @@ -0,0 +1,11 @@ +package com.example.api.reviewreport.dto; + +import com.example.api.domain.Review; + +public record ReviewReportRequest( + String reason +) { + public ReviewReportCommand toCommand(Review reviewId) { + return new ReviewReportCommand(reviewId, this.reason); + } +} diff --git a/src/main/java/com/example/api/reviewreport/dto/ReviewReportResponse.java b/src/main/java/com/example/api/reviewreport/dto/ReviewReportResponse.java new file mode 100644 index 00000000..6cb12af6 --- /dev/null +++ b/src/main/java/com/example/api/reviewreport/dto/ReviewReportResponse.java @@ -0,0 +1,6 @@ +package com.example.api.reviewreport.dto; + +public record ReviewReportResponse( + Long reportId, + String message +) {} diff --git a/src/main/java/com/example/api/search/SearchRepository.java b/src/main/java/com/example/api/search/SearchRepository.java new file mode 100644 index 00000000..75f8c2d9 --- /dev/null +++ b/src/main/java/com/example/api/search/SearchRepository.java @@ -0,0 +1,27 @@ +package com.example.api.search; + +import com.example.api.domain.Account; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; + +public interface SearchRepository extends JpaRepository { + + @Query("SELECT DISTINCT a FROM Account a " + + "JOIN PossibleBoard pb ON pb.employee = a " + + "JOIN Category c ON c.account = a " + + "WHERE (:category IS NULL OR c.categoryName = :category) " + + "AND (:startTime IS NULL OR pb.startTime <= :startTime) " + + "AND (:endTime IS NULL OR pb.endTime >= :endTime)") + List searchAccountsByCategoryAndTime( + @Param("category") String category, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime + ); +} + + + diff --git a/src/main/java/com/example/api/search/SearchService.java b/src/main/java/com/example/api/search/SearchService.java new file mode 100644 index 00000000..e1e75a7a --- /dev/null +++ b/src/main/java/com/example/api/search/SearchService.java @@ -0,0 +1,35 @@ +package com.example.api.search; + +import com.example.api.search.dto.SearchCommand; +import com.example.api.search.dto.SearchResponse; +import com.example.api.domain.Account; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class SearchService { + private final SearchRepository searchRepository; + + @Transactional(readOnly = true) + public List searchAccounts(final SearchCommand command) { + final List accounts = searchRepository.searchAccountsByCategoryAndTime( + command.category(), + command.startTime(), + command.endTime() + ); + return accounts.stream() + .map(account -> new SearchResponse( + account.getName(), + account.getSex(), + account.getAge(), + account.getStarPoint(), + account.getWorkCount() + )) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/api/search/controller/SearchController.java b/src/main/java/com/example/api/search/controller/SearchController.java new file mode 100644 index 00000000..b8d56791 --- /dev/null +++ b/src/main/java/com/example/api/search/controller/SearchController.java @@ -0,0 +1,32 @@ +package com.example.api.search.controller; + +import com.example.api.search.SearchService; +import com.example.api.search.dto.SearchCommand; +import com.example.api.search.dto.SearchRequest; +import com.example.api.search.dto.SearchResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/search") +public class SearchController { + private final SearchService searchService; + + @PostMapping("/search") + public ResponseEntity> searchAccounts( + @RequestBody @Validated final SearchRequest request + ) { + final SearchCommand command = new SearchCommand( + request.category(), + request.startTime(), + request.endTime() + ); + final List results = searchService.searchAccounts(command); + return ResponseEntity.ok(results); + } +} + diff --git a/src/main/java/com/example/api/search/dto/SearchCommand.java b/src/main/java/com/example/api/search/dto/SearchCommand.java new file mode 100644 index 00000000..6dfc0f5f --- /dev/null +++ b/src/main/java/com/example/api/search/dto/SearchCommand.java @@ -0,0 +1,13 @@ +package com.example.api.search.dto; + +import java.time.LocalDateTime; + +public record SearchCommand( + String category, + LocalDateTime startTime, + LocalDateTime endTime +) {} + + + + diff --git a/src/main/java/com/example/api/search/dto/SearchRequest.java b/src/main/java/com/example/api/search/dto/SearchRequest.java new file mode 100644 index 00000000..fa269bde --- /dev/null +++ b/src/main/java/com/example/api/search/dto/SearchRequest.java @@ -0,0 +1,10 @@ +package com.example.api.search.dto; + +import java.time.LocalDateTime; + +public record SearchRequest( + String category, + LocalDateTime startTime, + LocalDateTime endTime +) {} + diff --git a/src/main/java/com/example/api/search/dto/SearchResponse.java b/src/main/java/com/example/api/search/dto/SearchResponse.java new file mode 100644 index 00000000..9ed7732c --- /dev/null +++ b/src/main/java/com/example/api/search/dto/SearchResponse.java @@ -0,0 +1,9 @@ +package com.example.api.search.dto; + +public record SearchResponse( + String name, + String sex, + int age, + float starPoint, + int workCount +) {} diff --git a/src/main/java/com/example/api/suggest/controller/SuggestController.java b/src/main/java/com/example/api/suggest/controller/SuggestController.java new file mode 100644 index 00000000..62eea25f --- /dev/null +++ b/src/main/java/com/example/api/suggest/controller/SuggestController.java @@ -0,0 +1,25 @@ +package com.example.api.suggest.controller; + +import com.example.api.suggest.controller.dto.SuggestStatusDTO; +import com.example.api.suggest.controller.dto.request.BusinessIdRequest; +import com.example.api.suggest.service.SuggestService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class SuggestController { + private final SuggestService suggestService; + + @GetMapping("/api/v1/employment-suggests/status/{businessId}") + public ResponseEntity getSuggestStatus(@PathVariable() long businessId) { + BusinessIdRequest businessIdRequest = new BusinessIdRequest(businessId); + List suggestStatus = suggestService.getSuggestStatus(businessIdRequest); + return ResponseEntity.ok(suggestStatus); + } +} diff --git a/src/main/java/com/example/api/suggest/controller/dto/SuggestStatusDTO.java b/src/main/java/com/example/api/suggest/controller/dto/SuggestStatusDTO.java new file mode 100644 index 00000000..a0e86f8d --- /dev/null +++ b/src/main/java/com/example/api/suggest/controller/dto/SuggestStatusDTO.java @@ -0,0 +1,17 @@ +package com.example.api.suggest.controller.dto; + +import lombok.*; + + +@Getter +@Setter +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor +public class SuggestStatusDTO { + private String status; + private String name; + private String businessName; + private String workTime; + // 채팅 방 번호 추가 +} diff --git a/src/main/java/com/example/api/suggest/controller/dto/request/BusinessIdRequest.java b/src/main/java/com/example/api/suggest/controller/dto/request/BusinessIdRequest.java new file mode 100644 index 00000000..7d3fe7b5 --- /dev/null +++ b/src/main/java/com/example/api/suggest/controller/dto/request/BusinessIdRequest.java @@ -0,0 +1,4 @@ +package com.example.api.suggest.controller.dto.request; + +public record BusinessIdRequest(Long BusinessId) { +} diff --git a/src/main/java/com/example/api/suggest/service/SuggestService.java b/src/main/java/com/example/api/suggest/service/SuggestService.java new file mode 100644 index 00000000..0f9da16e --- /dev/null +++ b/src/main/java/com/example/api/suggest/service/SuggestService.java @@ -0,0 +1,75 @@ +package com.example.api.suggest.service; + +import com.example.api.domain.repository.OfferEmploymentRepository; +import com.example.api.domain.OfferEmployment; +import com.example.api.suggest.controller.dto.SuggestStatusDTO; +import com.example.api.suggest.controller.dto.request.BusinessIdRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class SuggestService { + private final OfferEmploymentRepository offerEmploymentRepository; + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + + @Transactional(readOnly = true) + public List getSuggestStatus(final BusinessIdRequest businessIdRequest) { + List offerList = offerEmploymentRepository.findAllByBusinessBusinessId(businessIdRequest.BusinessId()); + List suggestStatusDTOList = new ArrayList<>(); + + String status; + for (OfferEmployment offer : offerList) { + if (offer.isSuggestReaded() == false) { + status = "대기 중"; + } else if (offer.isSuggestReaded() == true && offer.isSuggestSucceeded() == false) { + status = "거절"; + } else if (offer.isSuggestReaded() && offer.isSuggestSucceeded() == true) { + if(offer.getContract().isContractSucceeded() == false) + status = "체결 중"; + else + status = "체결 완료"; + } else { + status = "알 수 없는 상태"; + } + List suggestList = offerEmploymentRepository.findSuggestByOfferEmploymentId(offer.getSuggestId()); + Object[] suggest = suggestList.get(0); + suggestStatusDTOList.add(makeSuggestStatusDTO(suggest, status)); + } + return suggestStatusDTOList; + } + + public SuggestStatusDTO makeSuggestStatusDTO(Object[] suggest, String status) { + String name = (String) suggest[0]; + String businessName = suggest[1].toString(); + LocalDateTime startTime = (LocalDateTime) suggest[2]; + LocalDateTime endTime = (LocalDateTime) suggest[3]; + + String formattedDate = startTime.format(formatter); + + StringBuilder workTime = new StringBuilder(); + workTime.append(formattedDate) + .append(" ") + .append(String.format("%02d", startTime.getHour())) + .append(":") + .append(String.format("%02d", startTime.getMinute())) + .append("~") + .append(String.format("%02d", endTime.getHour())) + .append(":") + .append(String.format("%02d", endTime.getMinute())); + String workTimeStr = workTime.toString(); + + return new SuggestStatusDTO( + status, + name, + businessName, + workTimeStr + ); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6e9b53b2..a4bd2071 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -21,3 +21,82 @@ spring.h2.console.path=/h2-console # Logging (Optional) logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE + +# Mongo Database Configuration +spring.data.mongodb.uri=mongodb://localhost:27017/testdb + +spring.data.mongodb.host=localhost +#spring.data.mongodb.port=27017 +#spring.data.mongodb.authentication-database=admin +#spring.data.mongodb.database=testdb +#spring.data.mongodb.auto-index-creation=true + +# SMTP +spring.mail.host=smtp.gmail.com +spring.mail.port=587 +spring.mail.username=${spring_mail_username} +spring.mail.password=${spring_mail_password} +spring.mail.protocol=smtp +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true +spring.mail.properties.mail.smtp.starttls.required=true +spring.mail.properties.mail.smtp.connectiontimeout=5000 +spring.mail.properties.mail.smtp.timeout=5000 +spring.mail.properties.mail.smtp.writetimeout=5000 +spring.mail.auth-code-expiration-millis=600000 + +# JWT +jwt.secret_key=${jwt.secret_key} +# 60? +jwt.access_token_valid_time=3600 +# 30? +jwt.refresh_token_valid_time=2592000 + +# mail code +code.length=6 +code.digit_range=10 + +# kakao Oauth2 ?? +spring.security.oauth2.client.registration.kakao.client-id=${KAKAO_CLIENT_ID} +spring.security.oauth2.client.registration.kakao.client-secret=${KAKAO_CLIENT_SECRET} +spring.security.oauth2.client.registration.kakao.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post +spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email + +# kakao Oauth2 Provider ?? +spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize +spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token +spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me +spring.security.oauth2.client.provider.kakao.user-name-attribute=id + +# naver Oauth2 ?? +spring.security.oauth2.client.registration.naver.client-id=${NAVER_CLIENT_ID} +spring.security.oauth2.client.registration.naver.client-secret=${NAVER_CLIENT_SECRET} +spring.security.oauth2.client.registration.naver.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.naver.client-authentication-method=client_secret_basic +spring.security.oauth2.client.registration.naver.scope=name,email + +# naver Oauth2 Provider ?? +spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize +spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token +spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me +spring.security.oauth2.client.provider.naver.user-name-attribute=response + +# redirect allow path +app.oauth2.authorized-redirect-uris=* + +# vendor +vendor.api.base-url=https://api.odcloud.kr/api/nts-businessman/v1/validate +vendor.api.service-key=${VENDOR_API_SERVICE-KEY} + +cloud.aws.s3.bucket=danpat +cloud.aws.region.static=ap-northeast-2 +cloud.aws.stack.auto=false +cloud.aws.credentials.accessKey=${AWS_CREDENTIALS_ACCESSKEY} +cloud.aws.credentials.secretKey=${AWS_CREDENTIALS_SECRETKEY} + +spring.servlet.multipart.enabled=true +spring.servlet.multipart.max-file-size=10MB +spring.servlet.multipart.max-request-size=10MB \ No newline at end of file diff --git a/src/test/java/com/example/api/ApiApplicationTests.java b/src/test/java/com/example/api/ApiApplicationTests.java index 315b8dc6..8a039658 100644 --- a/src/test/java/com/example/api/ApiApplicationTests.java +++ b/src/test/java/com/example/api/ApiApplicationTests.java @@ -2,12 +2,13 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("test") class ApiApplicationTests { @Test void contextLoads() { } - } diff --git a/src/test/java/com/example/api/JpaTestWithInitData.java b/src/test/java/com/example/api/JpaTestWithInitData.java new file mode 100644 index 00000000..c4aa17da --- /dev/null +++ b/src/test/java/com/example/api/JpaTestWithInitData.java @@ -0,0 +1,17 @@ +package com.example.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@Sql("/data.sql") +@DataJpaTest +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface JpaTestWithInitData { +} diff --git a/src/test/java/com/example/api/account/service/AccountServiceTest.java b/src/test/java/com/example/api/account/service/AccountServiceTest.java new file mode 100644 index 00000000..61997561 --- /dev/null +++ b/src/test/java/com/example/api/account/service/AccountServiceTest.java @@ -0,0 +1,62 @@ +package com.example.api.account.service; + +import com.example.api.account.dto.BusinessNumberRequest; +import com.example.api.account.dto.SignUpEmployerRequest; +import com.example.api.account.repository.AccountRepository; +import com.example.api.exception.BusinessException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@SpringBootTest +class UserServiceTest { + @MockBean + private AccountRepository accountRepository; + @Autowired + private AccountService accountService; + + @Test + @DisplayName("중복된 ID로 회원가입 시 예외가 발생해야 한다") + void shouldThrowExceptionWhenUserIdAlreadyExists() { + String userId = "existingUser"; + SignUpEmployerRequest mockRequest = mock(SignUpEmployerRequest.class); + when(mockRequest.loginId()).thenReturn(userId); + when(accountRepository.existsByLoginId(userId)).thenReturn(true); + + BusinessException thrown = assertThrows(BusinessException.class, + () -> accountService.signUpEmployer(mockRequest)); + assertEquals("중복된 ID입니다.", thrown.getErrorCode().getErrorDescription()); + } + + @Test + @DisplayName("유효하지 않은 사업자 번호로 검증 요청 시 예외가 발생해야 한다") + void shouldThrowExceptionWhenBusinessNumberIsInvalid() { + BusinessNumberRequest businessNumberRequest = new BusinessNumberRequest( + "1041736263 가짜지롱", + "20231123", + "김태영", + "김태영닷컴"); + + BusinessException thrown = assertThrows(BusinessException.class, + () -> accountService.verifyBusinessNumber(businessNumberRequest)); + assertEquals("사업자 등록 정보를 확인할 수 없습니다.", thrown.getErrorCode().getErrorDescription()); + } + + @Test + @DisplayName("유효한 사업자 번호로 검증 요청 시 성공해야 한다.") + void shouldReturnSuccessWhenValidBusinessNumberIsProvided() { + BusinessNumberRequest businessNumberRequest = new BusinessNumberRequest( + "1041736263", + "김태영닷컴", + "김태영", + "20231123"); + + String isValid = accountService.verifyBusinessNumber(businessNumberRequest); + assertEquals("유효한 사업자 등록 정보입니다.", isValid); + } +} diff --git a/src/test/java/com/example/api/announcement/AnnouncementServiceTest.java b/src/test/java/com/example/api/announcement/AnnouncementServiceTest.java new file mode 100644 index 00000000..c3a2932a --- /dev/null +++ b/src/test/java/com/example/api/announcement/AnnouncementServiceTest.java @@ -0,0 +1,117 @@ +package com.example.api.announcement; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +import com.example.api.announcement.dto.AnnouncementCommand; +import com.example.api.announcement.dto.AnnouncementResponse; +import com.example.api.domain.Announcement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.List; +import java.util.Optional; +import java.util.Arrays; + +@SpringBootTest +class AnnouncementServiceTest { + @MockBean + private AnnouncementRepository announcementRepository; + + @Autowired + private AnnouncementService announcementService; + + private Announcement announcement; + private AnnouncementCommand announcementCommand; + + public AnnouncementServiceTest() { + announcement = new Announcement(1L, "Test Title", "General", "Test Content", 0); + announcementCommand = new AnnouncementCommand(1L, "Test Title", "General", "Test Content"); + } + + @Test + @DisplayName("새로운 공지사항을 생성할 수 있어야 한다") + void shouldCreateAnnouncement() { + when(announcementRepository.save(any(Announcement.class))).thenReturn(announcement); + AnnouncementResponse response = announcementService.createAnnouncement(announcementCommand); + assertNotNull(response); + assertEquals("Test Title", response.announcementTitle()); + assertEquals("General", response.announcementType()); + assertEquals("Test Content", response.announcementContent()); + verify(announcementRepository, times(1)).save(any(Announcement.class)); + } + + @Test + @DisplayName("모든 공지사항을 조회할 수 있어야 한다") + void shouldGetAllAnnouncements() { + when(announcementRepository.findAll()).thenReturn(Arrays.asList(announcement)); + List responses = announcementService.getAllAnnouncements(); + assertNotNull(responses); + assertEquals(1, responses.size()); + assertEquals("Test Title", responses.get(0).announcementTitle()); + verify(announcementRepository, times(1)).findAll(); + } + + @Test + @DisplayName("특정 공지사항을 조회할 수 있어야 한다") + void shouldGetAnnouncement() { + when(announcementRepository.findById(1L)).thenReturn(Optional.of(announcement)); + AnnouncementResponse response = announcementService.getAnnouncement(1L); + assertNotNull(response); + assertEquals("Test Title", response.announcementTitle()); + verify(announcementRepository, times(1)).findById(1L); + } + + @Test + @DisplayName("공지사항이 존재하지 않으면 예외가 발생해야 한다") + void shouldThrowExceptionWhenAnnouncementNotFound() { + when(announcementRepository.findById(1L)).thenReturn(Optional.empty()); + RuntimeException thrown = assertThrows(RuntimeException.class, () -> { + announcementService.getAnnouncement(1L); + }); + assertEquals("announcement.not.found", thrown.getMessage()); + verify(announcementRepository, times(1)).findById(1L); + } + + @Test + @DisplayName("공지사항을 업데이트할 수 있어야 한다") + void shouldUpdateAnnouncement() { + when(announcementRepository.findById(1L)).thenReturn(Optional.of(announcement)); + when(announcementRepository.save(any(Announcement.class))).thenReturn(announcement); + AnnouncementResponse response = announcementService.updateAnnouncement(1L, announcementCommand); + assertNotNull(response); + assertEquals("Test Title", response.announcementTitle()); + assertEquals("General", response.announcementType()); + assertEquals("Test Content", response.announcementContent()); + + verify(announcementRepository, times(1)).findById(1L); + verify(announcementRepository, times(1)).save(any(Announcement.class)); + } + + @Test + @DisplayName("공지사항을 삭제할 수 있어야 한다") + void shouldDeleteAnnouncement() { + when(announcementRepository.findById(1L)).thenReturn(Optional.of(announcement)); + announcementService.deleteAnnouncement(1L, 1L); + verify(announcementRepository, times(1)).findById(1L); + verify(announcementRepository, times(1)).delete(any(Announcement.class)); + } + + @Test + @DisplayName("키워드를 사용해 공지사항을 검색할 수 있어야 한다") + void shouldSearchAnnouncements() { + when(announcementRepository.findByAnnouncementTitleContaining("Test")).thenReturn(Arrays.asList(announcement)); + List responses = announcementService.searchAnnouncements("Test"); + assertNotNull(responses); + assertEquals(1, responses.size()); + assertEquals("Test Title", responses.get(0).announcementTitle()); + verify(announcementRepository, times(1)).findByAnnouncementTitleContaining("Test"); + } +} + + + diff --git a/src/test/java/com/example/api/aws/service/S3ServiceTest.java b/src/test/java/com/example/api/aws/service/S3ServiceTest.java new file mode 100644 index 00000000..18ebe55d --- /dev/null +++ b/src/test/java/com/example/api/aws/service/S3ServiceTest.java @@ -0,0 +1,88 @@ +package com.example.api.aws.service; + +import com.example.api.account.entity.Nationality; +import com.example.api.account.entity.UserRole; +import com.example.api.account.repository.AccountRepository; +import com.example.api.aws.dto.UploadProfileRequest; +import com.example.api.aws.dto.UploadProfileResponse; +import com.example.api.domain.Account; +import jakarta.annotation.PostConstruct; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.ClassPathResource; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@SpringBootTest +class S3ServiceTest { + @Autowired + private S3Service s3Service; + @Autowired + private AccountRepository accountRepository; + + @PostConstruct + void setUp() { + accountRepository.deleteAll(); + Account account = new Account( + 25, // age + false, // deleted + "alice@example.com", // email + "user01", // loginId + "Alice", // name + Nationality.KOREAN, // nationality + "nickname1", // nickname + true, // openStatus + "pass01", // password + "010-1234-5678", // phoneNumber + "user-uploads/1/profile.png", // profileImage + List.of(UserRole.EMPLOYEE), // roles + "F", // sex + 4.5f, // starPoint + 10 // workCount + ); + accountRepository.save(account); + } + + @Test + @Order(1) + @DisplayName("업로드 파일이 null일 경우 기본 프로필로 초기화") + void uploadProfileImage_NullFile_ShouldInitializeToDefaultImage() { + MultipartFile file = null; + + UploadProfileRequest request = new UploadProfileRequest(1L, file); + + UploadProfileResponse response = s3Service.upload(request); + String newFile = accountRepository.findProfileImageByAccountId(1L).orElse(null); + assertNull(response.path()); + assertNull(newFile); // null로 업데이트 되었는지 확인 + } + + @Test + @Order(2) + @DisplayName("정상 업로드 성공") + void upload_ShouldUploadFileSuccessfully() throws IOException { + ClassPathResource resource = new ClassPathResource("test-files/test-image.png"); + MultipartFile file = new MockMultipartFile( + "file", + resource.getFilename(), + "image/png", + resource.getInputStream() + ); + + UploadProfileRequest request = new UploadProfileRequest(1L, file); + + UploadProfileResponse response = s3Service.upload(request); + String newFile = accountRepository.findProfileImageByAccountId(1L).orElse(null); + + assertNotNull(response); + assertEquals("https://danpat.s3.ap-northeast-2.amazonaws.com/user-uploads/1/profile.png", response.path()); + assertNotEquals("oldProfile", newFile); // 새로운 파일 이름으로 업데이트 되었는지 확인 + } +} \ No newline at end of file diff --git a/src/test/java/com/example/api/contracts/ContractServiceTest.java b/src/test/java/com/example/api/contracts/ContractServiceTest.java new file mode 100644 index 00000000..e2c43b3a --- /dev/null +++ b/src/test/java/com/example/api/contracts/ContractServiceTest.java @@ -0,0 +1,59 @@ +package com.example.api.contracts; + +import com.example.api.JpaTestWithInitData; +import com.example.api.chat.repository.ChatRoomRepository; +import com.example.api.contracts.dto.AcceptSuggestCommand; +import com.example.api.domain.ChatRoom; +import com.example.api.domain.Contract; +import com.example.api.domain.OfferEmployment; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.*; + +@ActiveProfiles("test") +@JpaTestWithInitData +class ContractServiceTest{ + @Autowired + OfferRepository offerRepository; + @Autowired + ChatRoomRepository chatRoomRepository; + + @Test + @DisplayName("요청 수락 및 채팅방 생성") + void shouldAcceptSuggestAndCreateChatRoom() { + AcceptSuggestCommand acceptSuggestCommand = new AcceptSuggestCommand(1L); + + // 요청 수락 + final OfferEmployment offerEmployment = loadOffer(acceptSuggestCommand.suggestId()); + offerEmployment.succeeded(); + // 채팅방 생성 + createChatRoom(offerEmployment); + + OfferEmployment findOfferEmployment = offerRepository.findById(1L) + .orElseThrow(() -> new AssertionError("요청이 존재하지 않습니다.")); + + assertThat(findOfferEmployment.isSuggestSucceeded()) + .as("요청 수락으로 변경") + .isTrue(); + + // Assert: 채팅방 생성 여부 검증 + assertThat(chatRoomRepository.findById(1L)) + .as("채팅방이 존재하지 않음") + .isPresent(); + } + + private OfferEmployment loadOffer(final Long offerId) { + return offerRepository.findById(offerId) + .orElseThrow(); + } + + private void createChatRoom(final OfferEmployment offer) { + ChatRoom chatRoom = new ChatRoom(offer); + chatRoomRepository.save(chatRoom); + } + + +} \ No newline at end of file diff --git a/src/test/java/com/example/api/employer/service/EmployerServiceTest.java b/src/test/java/com/example/api/employer/service/EmployerServiceTest.java new file mode 100644 index 00000000..a21c360b --- /dev/null +++ b/src/test/java/com/example/api/employer/service/EmployerServiceTest.java @@ -0,0 +1,58 @@ +package com.example.api.employer.service; + +import com.example.api.account.entity.Location; +import com.example.api.account.repository.AccountRepository; +import com.example.api.account.repository.LocationRepository; +import com.example.api.board.dto.request.EmployeeIdRequest; +import com.example.api.business.BusinessRepository; +import com.example.api.domain.Account; +import com.example.api.domain.Business; +import com.example.api.employer.controller.dto.EmployerBusinessesRequest; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.ArrayList; +import java.util.List; + +@SpringBootTest +class EmployerServiceTest { + @Autowired + private EmployerService employerService; + @Autowired + private AccountRepository accountRepository; + @Autowired + private BusinessRepository businessRepository; + @Autowired + private LocationRepository locationRepository; + private List businessesList = new ArrayList<>(); + + @BeforeEach + void setUp() { + Account account = new Account(); + accountRepository.save(account); + Location location1 = new Location("zipcode1", "address1", "detailAddress1"); + Location location2 = new Location("zipcode2", "address2", "detailAddress2"); + Location location3 = new Location("zipcode3", "address3", "detailAddress3"); + locationRepository.save(location1); + locationRepository.save(location2); + locationRepository.save(location3); + Business business1 = new Business(account, "가게명1", location1); + Business business2 = new Business(account, "가게명2", location2); + Business business3 = new Business(account, "가게명3", location3); + businessRepository.save(business1); + businessRepository.save(business2); + businessRepository.save(business3); + businessesList.add(new EmployerBusinessesRequest(business1.getBusinessName(), business1.getLocation())); + businessesList.add(new EmployerBusinessesRequest(business2.getBusinessName(), business2.getLocation())); + businessesList.add(new EmployerBusinessesRequest(business3.getBusinessName(), business3.getLocation())); + } + + @Test + void testGetBusinessesByOwnerId(){ + List employerBusinessList = employerService.getEmployerBusinessList(new EmployeeIdRequest(1L)); + Assertions.assertThat(employerBusinessList).isEqualTo(businessesList); + } +} \ No newline at end of file diff --git a/src/test/java/com/example/api/global/BaseIntegrationTest.java b/src/test/java/com/example/api/global/BaseIntegrationTest.java new file mode 100644 index 00000000..e69de29b diff --git a/src/test/java/com/example/api/inquiry/InquiryServiceTest.java b/src/test/java/com/example/api/inquiry/InquiryServiceTest.java new file mode 100644 index 00000000..0cdc7191 --- /dev/null +++ b/src/test/java/com/example/api/inquiry/InquiryServiceTest.java @@ -0,0 +1,65 @@ +package com.example.api.inquiry; + +import com.example.api.domain.Inquiry; +import com.example.api.inquiry.dto.InquiryCommand; +import com.example.api.inquiry.dto.InquiryRequest; +import com.example.api.inquiry.dto.InquiryResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.any; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@SpringBootTest +class InquiryServiceTest { + + @MockBean + private InquiryRepository inquiryRepository; + + @Autowired + private InquiryService inquiryService; + + private InquiryRequest inquiryRequest; + private InquiryCommand inquiryCommand; + private Inquiry inquiry; + + public InquiryServiceTest() { + inquiryRequest = new InquiryRequest("Technical Support", "Billing Issue", "How to pay?", "I have a question about paying my bill."); + inquiry = new Inquiry(1L, "Technical Support", "Billing Issue", "How to pay?", "I have a question about paying my bill.", Inquiry.InquiryStatus.WAITING, null); + } + + @Test + @DisplayName("새로운 문의사항을 생성할 수 있어야 한다") + void shouldCreateInquiry() { + when(inquiryRepository.save(Mockito.any(Inquiry.class))).thenReturn(inquiry); + InquiryResponse response = inquiryService.saveInquiry(inquiryRequest, 1L); + assertNotNull(response); + assertEquals("Technical Support", response.inquiryType()); + assertEquals("Billing Issue", response.subInquiryType()); + assertEquals("How to pay?", response.title()); + verify(inquiryRepository, times(1)).save(Mockito.any(Inquiry.class)); + } + + @Test + @DisplayName("특정 사용자의 모든 문의사항을 조회할 수 있어야 한다") + void shouldGetInquiriesByAccountId() { + when(inquiryRepository.findByCreatedBy(1L)).thenReturn(Collections.singletonList(inquiry)); + List responses = inquiryService.getInquiriesByAccountId(1L); + assertNotNull(responses); + assertEquals(1, responses.size()); + assertEquals("Technical Support", responses.get(0).inquiryType()); + assertEquals("Billing Issue", responses.get(0).subInquiryType()); + verify(inquiryRepository, times(1)).findByCreatedBy(1L); + } +} + + + diff --git a/src/test/java/com/example/api/oauth2/service/CustomOauth2UserServiceTest.java b/src/test/java/com/example/api/oauth2/service/CustomOauth2UserServiceTest.java new file mode 100644 index 00000000..5690b59f --- /dev/null +++ b/src/test/java/com/example/api/oauth2/service/CustomOauth2UserServiceTest.java @@ -0,0 +1,153 @@ +package com.example.api.oauth2.service; + +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.user.OAuth2User; +import com.github.tomakehurst.wiremock.WireMockServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class CustomOauth2UserServiceTest { + @Autowired + private CustomOauth2UserService customOauth2UserService; + private WireMockServer kakaoWireMockServer; + private WireMockServer naverWireMockServer; + + @BeforeEach + void setupWireMock() { + setKakaoWireMockServer(); + setNaverWireMockServer(); + } + + @AfterEach + void teardownWireMock() { + if (kakaoWireMockServer != null) { + kakaoWireMockServer.stop(); + } + + if (naverWireMockServer != null) { + naverWireMockServer.stop(); + } + } + + @Test + void loadUser_kakao() { + ClientRegistration clientRegistration = setKakaoClientRegistration(); + OAuth2AccessToken token = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + "mock-access-token", + null, + null + ); + OAuth2UserRequest mockRequest = new OAuth2UserRequest(clientRegistration, token); + + OAuth2User result = customOauth2UserService.loadUser(mockRequest); + assertNotNull(result); + assertEquals("danpat@kakao.com", result.getAttribute("email")); + assertEquals("단팥", result.getAttribute("name")); + } + + @Test + void loadUser_naver() { + ClientRegistration clientRegistration = setNaverClientRegistration(); + OAuth2AccessToken token = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + "mock-access-token", + null, + null + ); + OAuth2UserRequest mockRequest = new OAuth2UserRequest(clientRegistration, token); + + OAuth2User result = customOauth2UserService.loadUser(mockRequest); + assertNotNull(result); + assertEquals("danpat@naver.com", result.getAttribute("email")); + assertEquals("단팥", result.getAttribute("name")); + } + + private void setNaverWireMockServer() { + naverWireMockServer = new WireMockServer(8090); // 포트 설정 + naverWireMockServer.start(); + naverWireMockServer.stubFor(get(urlEqualTo("/v1/nid/me")) + .withHeader("Authorization", matching("Bearer .*")) // Bearer 토큰 매칭 + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withStatus(200) + .withBody(""" + { + "resultcode": "00", + "message": "success", + "response": { + "id": "1234567890", + "email": "danpat@naver.com", + "name": "단팥" + } + }"""))); + } + + private void setKakaoWireMockServer() { + kakaoWireMockServer = new WireMockServer( + WireMockConfiguration.wireMockConfig().port(8089) + ); + kakaoWireMockServer.start(); + kakaoWireMockServer.stubFor(get(urlEqualTo("/v2/user/me")) + .withHeader("Authorization", matching("Bearer .*")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody(""" + { + "id": "0000000000", + "properties": { + "nickname": "단팥" + }, + "kakao_account": { + "email": "danpat@kakao.com" + } + }"""))); + } + + @NotNull + private ClientRegistration setKakaoClientRegistration() { + return ClientRegistration.withRegistrationId("kakao") + .clientId("mock-client-id") + .clientSecret("mock-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/kakao") + .scope("profile", "email") + .authorizationUri("http://localhost:8089/oauth/authorize") + .tokenUri("http://localhost:8089/oauth/token") + .userInfoUri("http://localhost:8089/v2/user/me") + .userNameAttributeName("id") + .clientName("Kakao Mock Client") + .build(); + } + + @NotNull + private ClientRegistration setNaverClientRegistration() { + return ClientRegistration.withRegistrationId("naver") + .clientId("mock-client-id") + .clientSecret("mock-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/naver") + .scope("profile", "email") + .authorizationUri("http://localhost:8090/oauth/authorize") + .tokenUri("http://localhost:8090/oauth/token") + .userInfoUri("http://localhost:8090/v1/nid/me") + .userNameAttributeName("response") + .clientName("Kakao Mock Client") + .build(); + } +} diff --git a/src/test/java/com/example/api/offeremployment/OfferEmploymentServiceTest.java b/src/test/java/com/example/api/offeremployment/OfferEmploymentServiceTest.java new file mode 100644 index 00000000..c1a22592 --- /dev/null +++ b/src/test/java/com/example/api/offeremployment/OfferEmploymentServiceTest.java @@ -0,0 +1,132 @@ +package com.example.api.offeremployment; + +import com.example.api.account.entity.Nationality; +import com.example.api.account.entity.UserRole; +import com.example.api.account.repository.AccountRepository; +import com.example.api.business.BusinessRepository; +import com.example.api.domain.Account; +import com.example.api.domain.Business; +import com.example.api.domain.OfferEmployment; +import com.example.api.domain.repository.OfferEmploymentRepository; +import com.example.api.offeremployment.dto.OfferEmploymentRequest; +import com.example.api.offeremployment.dto.OfferEmploymentResponse; +import jakarta.annotation.PostConstruct; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class OfferEmploymentServiceTest { + + @Autowired + private OfferEmploymentService offerEmploymentService; + + @Autowired + private AccountRepository accountRepository; + + @Autowired + private BusinessRepository businessRepository; + + @Autowired + private OfferEmploymentRepository offerEmploymentRepository; + + @PostConstruct + void setUp() { + offerEmploymentRepository.deleteAll(); + businessRepository.deleteAll(); + accountRepository.deleteAll(); + + Account employee = new Account( + "employee01", "password123", "Alice", "nickname1", "010-1234-5678", + "alice@example.com", Nationality.KOREAN, List.of(UserRole.EMPLOYEE), true + ); + accountRepository.save(employee); + + Account employer = new Account( + "employer01", "password456", "Bob", "nickname2", "010-9876-5432", + "bob@example.com", Nationality.KOREAN, List.of(UserRole.EMPLOYER), true + ); + accountRepository.save(employer); + } + Business business = new Business( + "My Coffee Shop", + "서울시 강남구", + "Bob", + employer, + LocalDate.of(2020, 1, 1), + "123-45-67890" + ); + businessRepository.save(business); + + @Test + @Order(1) + @DisplayName("정상적으로 고용 제안을 보낼 수 있다") + void sendOfferEmployment_ShouldSucceed() { + OfferEmploymentRequest request = new OfferEmploymentRequest( + 1L, + 1L, + 15000, + LocalDateTime.of(2025, 1, 1, 9, 0), + LocalDateTime.of(2025, 1, 1, 18, 0) + ); + + OfferEmploymentResponse response = offerEmploymentService.sendOfferEmployment(request); + + assertNotNull(response); + assertTrue(response.success()); + assertEquals("Offer succeeded", response.message()); + + Optional savedOfferEmployment = offerEmploymentRepository.findAll().stream().findFirst(); + assertTrue(savedOfferEmployment.isPresent()); + OfferEmployment offerEmployment = savedOfferEmployment.get(); + assertEquals(1L, offerEmployment.getEmployee().getAccountId()); + assertEquals(1L, offerEmployment.getBusiness().getBusinessId()); + assertEquals(15000, offerEmployment.getSuggestHourlyPay()); + } + + @Test + @Order(2) + @DisplayName("잘못된 알바 ID로 고용 제안을 보낼 경우 실패한다") + void sendOfferEmployment_InvalidEmployee_ShouldFail() { + OfferEmploymentRequest request = new OfferEmploymentRequest( + 999L, + 1L, + 15000, + LocalDateTime.of(2025, 1, 1, 9, 0), + LocalDateTime.of(2025, 1, 1, 18, 0) + ); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> offerEmploymentService.sendOfferEmployment(request) + ); + assertEquals("Account not found with ID: 999", exception.getMessage()); + } + + @Test + @Order(3) + @DisplayName("잘못된 비즈니스 ID로 고용 제안을 보낼 경우 실패한다") + void sendOfferEmployment_InvalidBusiness_ShouldFail() { + OfferEmploymentRequest request = new OfferEmploymentRequest( + 1L, + 999L, + 15000, + LocalDateTime.of(2025, 1, 1, 9, 0), + LocalDateTime.of(2025, 1, 1, 18, 0) + ); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> offerEmploymentService.sendOfferEmployment(request) + ); + assertEquals("Business not found with ID: 999", exception.getMessage()); + } +} + diff --git a/src/test/java/com/example/api/review/ReviewServiceTest.java b/src/test/java/com/example/api/review/ReviewServiceTest.java new file mode 100644 index 00000000..4b6fb0e9 --- /dev/null +++ b/src/test/java/com/example/api/review/ReviewServiceTest.java @@ -0,0 +1,103 @@ +package com.example.api.review; + +import com.example.api.domain.Contract; +import com.example.api.domain.Review; +import com.example.api.review.dto.ReviewCommand; +import com.example.api.review.dto.ReviewResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.*; + +@SpringBootTest +class ReviewServiceTest { + + @MockBean + private ReviewRepository reviewRepository; + + @Autowired + private ReviewService reviewService; + + private ReviewCommand reviewCommand; + private List reviews; + private Review review; + + @BeforeEach + public void setUp() { + reviewCommand = new ReviewCommand(1L); + Contract someContract = new Contract(); + review = new Review( + 5, + "Great job!", + someContract + ); + reviews = List.of(review); + } + + @Test + @DisplayName("모든 리뷰를 조회할 수 있어야 한다") + void shouldGetAllReviews() { + when(reviewRepository.findReviewsByDynamicQuery(null)).thenReturn(reviews); + + List response = reviewService.getAllReviews(); + + assertNotNull(response); + assertEquals(1, response.size()); + assertEquals("Great job!", response.get(0).reviewContent()); + + verify(reviewRepository, times(1)).findReviewsByDynamicQuery(null); + } + + @Test + @DisplayName("특정 리뷰 ID로 리뷰를 조회할 수 있어야 한다") + void shouldGetReviewsByEmployee() { + when(reviewRepository.findReviewsByDynamicQuery(1L)).thenReturn(reviews); + + List response = reviewService.getReviewsByEmployee(1L); + + assertNotNull(response); + assertEquals(1, response.size()); + assertEquals("Great job!", response.get(0).reviewContent()); + + verify(reviewRepository, times(1)).findReviewsByDynamicQuery(1L); + } + + @Test + @DisplayName("특정 사용자의 모든 리뷰를 조회할 수 있어야 한다") + void shouldGetReviewsByAccountId() { + when(reviewRepository.findReviewsByEmployee_AccountId(reviewCommand.accountId())).thenReturn(reviews); + + List response = reviewService.getReviews(reviewCommand); + + assertNotNull(response); + assertEquals(1, response.size()); + assertEquals("Great job!", response.get(0).reviewContent()); + + verify(reviewRepository, times(1)).findReviewsByEmployee_AccountId(reviewCommand.accountId()); + } + + @Test + @DisplayName("특정 사용자의 상세 리뷰를 조회할 수 있어야 한다") + void shouldGetReviewsByEmployeeWithDetails() { + when(reviewRepository.findReviewsByAccountIdWithDetails(reviewCommand.accountId())).thenReturn(reviews); + + List response = reviewService.getReviewsByEmployeeWithDetails(reviewCommand); + + assertNotNull(response); + assertEquals(1, response.size()); + assertEquals("Great job!", response.get(0).reviewContent()); + + verify(reviewRepository, times(1)).findReviewsByAccountIdWithDetails(reviewCommand.accountId()); + } +} + + + diff --git a/src/test/java/com/example/api/reviewavailable/ReviewAvailableServiceTest.java b/src/test/java/com/example/api/reviewavailable/ReviewAvailableServiceTest.java new file mode 100644 index 00000000..4881a1b5 --- /dev/null +++ b/src/test/java/com/example/api/reviewavailable/ReviewAvailableServiceTest.java @@ -0,0 +1,109 @@ +package com.example.api.reviewavailable; + +import com.example.api.account.entity.UserRole; +import com.example.api.contracts.ContractRepository; +import com.example.api.domain.Account; +import com.example.api.domain.Business; +import com.example.api.domain.Contract; +import com.example.api.domain.OfferEmployment; +import com.example.api.reviewavailable.dto.ReviewAvailableCommand; +import com.example.api.reviewavailable.dto.ReviewAvailableResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@SpringBootTest +class ReviewAvailableServiceTest { + + @Autowired + private ContractRepository contractRepository; + + @Autowired + private ReviewAvailableService reviewAvailableService; + + private ReviewAvailableCommand reviewAvailableCommand; + private List contracts; + private Contract completedContract; + private Contract incompleteContract; + + @BeforeEach + public void setUp() { + Account employer = new Account("Alice Employer", "alice@business.com", List.of(UserRole.EMPLOYER)); + Business business = new Business(employer, "Test Business"); + + Account employee1 = new Account("John Doe", "john.doe@email.com", List.of(UserRole.EMPLOYEE)); + Account employee2 = new Account("Jane Smith", "jane.smith@email.com", List.of(UserRole.EMPLOYEE)); + + OfferEmployment offerEmployment1 = new OfferEmployment( + business, + employee1, + LocalDateTime.of(2025, 1, 1, 9, 0), + LocalDateTime.of(2025, 1, 1, 18, 0), + 10000 + ); + + OfferEmployment offerEmployment2 = new OfferEmployment( + business, + employee2, + LocalDateTime.of(2025, 1, 2, 9, 0), + LocalDateTime.of(2025, 1, 2, 18, 0), + 12000 + ); + + completedContract = new Contract( + offerEmployment1, + LocalDateTime.of(2025, 1, 1, 9, 0), + LocalDateTime.of(2025, 1, 1, 18, 0), + 10000, + true + ); + + incompleteContract = new Contract( + offerEmployment2, + LocalDateTime.of(2025, 1, 2, 9, 0), + LocalDateTime.of(2025, 1, 2, 18, 0), + 12000, + false + ); + + contracts = List.of(completedContract, incompleteContract); + + reviewAvailableCommand = new ReviewAvailableCommand(business.getBusinessId()); + } + + @Test + @DisplayName("완료된 계약만 조회할 수 있어야 한다") + void shouldReturnCompletedContracts() { + when(contractRepository.findAvailableReviewsByBusinessId(reviewAvailableCommand.businessId())) + .thenReturn( + contracts.stream() + .filter(Contract::isContractSucceeded) + .map(contract -> new ReviewAvailableResponse( + contract.getOfferEmployment().getEmployee().getAccountId(), + contract.getOfferEmployment().getEmployee().getName() + )) + .toList() + ); + List responses = reviewAvailableService.getAvailableReviewTargets(reviewAvailableCommand); + + assertNotNull(responses); + assertEquals(1, responses.size()); + assertEquals(completedContract.getOfferEmployment().getEmployee().getName(), responses.get(0).employeeName()); + assertEquals(completedContract.getOfferEmployment().getEmployee().getAccountId(), responses.get(0).employeeId()); + + verify(contractRepository, times(1)).findAvailableReviewsByBusinessId(reviewAvailableCommand.businessId()); + + } +} + + + + diff --git a/src/test/java/com/example/api/reviewreport/ReviewReportServiceTest.java b/src/test/java/com/example/api/reviewreport/ReviewReportServiceTest.java new file mode 100644 index 00000000..cfe7dcf0 --- /dev/null +++ b/src/test/java/com/example/api/reviewreport/ReviewReportServiceTest.java @@ -0,0 +1,72 @@ +package com.example.api.reviewreport; + +import com.example.api.domain.Contract; +import com.example.api.domain.Review; +import com.example.api.domain.ReviewReport; +import com.example.api.reviewreport.dto.ReviewReportCommand; +import com.example.api.reviewreport.dto.ReviewReportResponse; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@SpringBootTest +class ReviewReportServiceTest { + @Autowired + private ReviewReportRepository reviewReportRepository; + + @Autowired + private ReviewReportService reviewReportService; + + private Review review; + private ReviewReportCommand reviewReportCommand; + + @BeforeEach + void setUp() { + Contract contract = new Contract( + null, + LocalDateTime.of(2025, 1, 1, 9, 0), + LocalDateTime.of(2025, 1, 1, 18, 0), + 10000, + true + ); + review = new Review(5, "Excellent work!", contract); + reviewReportCommand = new ReviewReportCommand(review, "Inappropriate content"); + } + + @Test + @DisplayName("리뷰 신고가 성공적으로 처리되어야 한다") + void shouldReportReviewSuccessfully() { + when(reviewReportRepository.existsByReview(review)).thenReturn(false); + when(reviewReportRepository.save(any(ReviewReport.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + ReviewReportResponse response = reviewReportService.reportReview(reviewReportCommand); + + assertNotNull(response); + assertEquals("리뷰 신고가 성공적으로 처리되었습니다.", response.message()); + + verify(reviewReportRepository, times(1)).existsByReview(review); + verify(reviewReportRepository, times(1)).save(any(ReviewReport.class)); + } + + @Test + @DisplayName("이미 신고된 리뷰라면 예외를 던져야 한다") + void shouldThrowExceptionIfReviewAlreadyReported() { + when(reviewReportRepository.existsByReview(review)).thenReturn(true); + + IllegalStateException exception = assertThrows( + IllegalStateException.class, + () -> reviewReportService.reportReview(reviewReportCommand) + ); + assertEquals("이미 신고된 리뷰입니다.", exception.getMessage()); + + verify(reviewReportRepository, times(1)).existsByReview(review); + verify(reviewReportRepository, times(0)).save(any(ReviewReport.class)); + } +} + diff --git a/src/test/java/com/example/api/search/SearchIntegrationTest.java b/src/test/java/com/example/api/search/SearchIntegrationTest.java new file mode 100644 index 00000000..14de0a1a --- /dev/null +++ b/src/test/java/com/example/api/search/SearchIntegrationTest.java @@ -0,0 +1,65 @@ +package com.example.api.search; + +import com.example.api.search.dto.SearchCommand; +import com.example.api.search.dto.SearchResponse; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + +@SpringBootTest +@AutoConfigureMockMvc +class SearchIntegrationTest extends BaseIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private SearchService searchService; + + @Test + void testSearchAccountsFromService() { + LocalDateTime startTime = LocalDateTime.of(2024, 11, 5, 9, 0); + LocalDateTime endTime = LocalDateTime.of(2024, 11, 5, 17, 0); + + SearchCommand command = new SearchCommand("IT Services", startTime, endTime); + + List results = searchService.searchAccounts(command); + + assertEquals(1, results.size()); + SearchResponse response = results.get(0); + assertEquals("John Doe", response.name()); + assertEquals("Male", response.sex()); + assertEquals(30, response.age()); + assertEquals(3.5f, response.starPoint()); + assertEquals(3, response.workCount()); + } + + @Test + void testSearchAccountsFromController() throws Exception { + mockMvc.perform(post("/api/search/search") + .contentType("application/json") + .content(""" + { + "category": "IT Services", + "startTime": "2024-11-05T09:00:00", + "endTime": "2024-11-05T17:00:00" + } + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name").value("John Doe")) + .andExpect(jsonPath("$[0].sex").value("Male")) + .andExpect(jsonPath("$[0].age").value(30)) + .andExpect(jsonPath("$[0].starPoint").value(3.5)) + .andExpect(jsonPath("$[0].workCount").value(3)); + } +} diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 00000000..33d21693 --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,41 @@ +spring.config.import=optional:file:.env[.properties] + +spring.jpa.hibernate.ddl-auto=create +spring.sql.init.mode=always + +# kakao Oauth2 ?? +spring.security.oauth2.client.registration.kakao.client-id=${KAKAO_CLIENT_ID} +spring.security.oauth2.client.registration.kakao.client-secret=${KAKAO_CLIENT_SECRET} +spring.security.oauth2.client.registration.kakao.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post +spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email + +# kakao Oauth2 Provider ?? +spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize +spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token +spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me +spring.security.oauth2.client.provider.kakao.user-name-attribute=id + +# naver Oauth2 ?? +spring.security.oauth2.client.registration.naver.client-id=${NAVER_CLIENT_ID} +spring.security.oauth2.client.registration.naver.client-secret=${NAVER_CLIENT_SECRET} +spring.security.oauth2.client.registration.naver.redirect-uri={baseUrl}/oauth2/callback/{registrationId} +spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.naver.client-authentication-method=client_secret_basic +spring.security.oauth2.client.registration.naver.scope=name,email + +# naver Oauth2 Provider ?? +spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize +spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token +spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me +spring.security.oauth2.client.provider.naver.user-name-attribute=response + +# redirect allow path +app.oauth2.authorized-redirect-uris=* + +cloud.aws.s3.bucket=danpat +cloud.aws.region.static=ap-northeast-2 +cloud.aws.stack.auto=false +cloud.aws.credentials.accessKey=${AWS_CREDENTIALS_ACCESSKEY} +cloud.aws.credentials.secretKey=${AWS_CREDENTIALS_SECRETKEY} \ No newline at end of file diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql new file mode 100644 index 00000000..7a2b1dd4 --- /dev/null +++ b/src/test/resources/data.sql @@ -0,0 +1,102 @@ +-- ACCOUNT 테이블 초기화 데이터 +INSERT INTO account ( + account_unique_id, account_id, account_password, account_name, account_nickname, account_sex, + account_age, account_phone_number, account_registered_datetime, account_profile_image, + account_email, account_deleted, updated_at, account_nationality, account_role, account_star_rating, account_work_count, account_open_status +) VALUES + (1, 'user01', 'pass01', 'Alice', 'nickname1', 'F', 25, '010-1234-5678', CURRENT_TIMESTAMP, 'profile1.jpg', 'alice@example.com', false, CURRENT_TIMESTAMP, 'KOREAN', 'EMPLOYEE', 0, 0, true), + (2, 'user02', 'pass02', 'Bob', 'nickname2', 'M', 30, '010-2345-6789', CURRENT_TIMESTAMP, 'profile2.jpg', 'bob@example.com', false, CURRENT_TIMESTAMP, 'KOREAN', 'EMPLOYEE', 0, 0, true); + +-- EMPLOYER 테이블 초기화 데이터 +INSERT INTO employer ( + employer_id, account_id, employer_nickname, created_at, updated_at +) VALUES + (1, 1, 'TopEmployer', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +-- EMPLOYEE 테이블 초기화 데이터 +INSERT INTO employee ( + employee_id, account_id, employee_star_rating, employee_work_count, employee_nickname, created_at, updated_at +) VALUES + (1, 2, 4.5, 100, 'ReliableEmployee', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +-- CATEGORY 테이블 초기화 데이터 +INSERT INTO category ( + category_id, category_name, created_at, updated_at +) VALUES + (1, 'Technology', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + (2, 'Healthcare', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +-- BUSINESS 테이블 초기화 데이터 +INSERT INTO business ( + business_id, business_employer_id, business_name, business_location, representation_name, + business_open_date, business_registration_number, created_at, updated_at +) VALUES + (1, 1, 'Tech Solutions', 'Seoul', 'Alice Kim', '2020-01-15', '123-45-67890', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +-- BUSINESS_CATEGORY 테이블 초기화 데이터 +INSERT INTO business_category ( + business_category_id, business_id, categorey_id, created_at, updated_at +) VALUES + (1, 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + (2, 1, 2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +-- ANNOUNCEMENT 테이블 초기화 데이터 +INSERT INTO announcement ( + announcement_id, announcement_title, announcement_type, announcement_content, view_count, created_at, updated_at +) VALUES + (1, 'New Policy', 'HR', 'Please review the new company policy.', 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +-- OFFER_EMPLOYMENT 테이블 초기화 데이터 +INSERT INTO offer_employment ( + suggest_id, suggest_hourly_pay, suggest_readed, suggest_succeded, business_id, employee_id, + suggest_register_time, suggest_start_time, suggest_end_time +) VALUES + (1, 20000, false, false, 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +-- CHAT_ROOM 테이블 초기화 데이터 +INSERT INTO chat_room ( + chat_room_id, suggest_generated_date, suggest_id +) VALUES + (1, CURRENT_TIMESTAMP, 1); + +-- CHAT 테이블 초기화 데이터 +INSERT INTO chat ( + chat_id, account_unique_id, chat_register_date, chat_room_id, chat_content, chat_deleted +) VALUES + (1, 1, CURRENT_TIMESTAMP, 1, 'Hello, welcome to the chat room!', false); + +-- CONTRACT 테이블 초기화 데이터 +INSERT INTO contract ( + contract_id, contract_hourly_pay, contract_succeded, contract_start_time, contract_end_time, suggest_id, created_at, updated_at +) VALUES + (1, 25000, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +-- EXTERNAL_CAREER 테이블 초기화 데이터 +INSERT INTO exteranl_carrer ( + id, employee_id, business_name, part_time_period, created_at, updated_at +) VALUES + (1, 1, 'Part-time Developer', '2019-2020', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +-- FLAVORED 테이블 초기화 데이터 +INSERT INTO flavored ( + flavored_id, category_id, employee_id, created_at, updated_at +) VALUES + (1, 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +-- POSSIBLE_BOARD 테이블 초기화 데이터 +INSERT INTO possible_board ( + possible_id, employee_id, possible_start_time, possible_end_time, created_at, updated_at +) VALUES + (1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +-- REVIEW 테이블 초기화 데이터 +INSERT INTO review ( + suggest_id, review_star_point, review_content, created_at, updated_at +) VALUES + (1, 5, 'Excellent service!', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +-- SCRAP 테이블 초기화 데이터 +INSERT INTO scrap ( + scrap_id, employee_employee_id, employer_employer_id, created_at, updated_at +) VALUES + (1, 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 00000000..8d3e2de4 --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,290 @@ +drop table if exists account cascade; +drop table if exists announcement cascade; +drop table if exists business cascade; +drop table if exists business_category cascade; +drop table if exists category cascade; +drop table if exists chat cascade; +drop table if exists chat_room cascade; +drop table if exists contract cascade; +drop table if exists employee cascade; +drop table if exists employer cascade; +drop table if exists exteranl_carrer cascade; +drop table if exists flavored cascade; +drop table if exists offer_employment cascade; +drop table if exists possible_board cascade ; +drop table if exists review cascade; +drop table if exists scrap cascade; + +create table account +( + account_age integer, + account_deleted BOOLEAN DEFAULT false, + account_registered_datetime timestamp(6), + account_unique_id bigint generated by default as identity, + updated_at timestamp(6), + account_email varchar(255), + account_id varchar(255), + account_name varchar(255), + account_nickname varchar(255), + account_nationality varchar(255), + account_role varchar(255), + account_password varchar(255), + account_phone_number varchar(255), + account_profile_image varchar(255), + account_sex varchar(255), + account_star_rating float, + account_work_count integer, + account_open_status BOOLEAN DEFAULT true, + primary key (account_unique_id) +); +create table announcement +( + view_count int DEFAULT 0, + announcement_id bigint generated by default as identity, + created_at timestamp(6), + updated_at timestamp(6), + announcement_content varchar(255), + announcement_title varchar(255), + announcement_type varchar(255), + primary key (announcement_id) +); + +create table business +( + business_open_date date, + business_employer_id bigint, + business_id bigint generated by default as identity, + created_at timestamp(6), + updated_at timestamp(6), + business_location varchar(255), + business_name varchar(255), + business_registration_number varchar(255), + representation_name varchar(255), + primary key (business_id) +); + +create table business_category +( + business_category_id bigint generated by default as identity, + business_id bigint, + categorey_id bigint, + created_at timestamp(6), + updated_at timestamp(6), + primary key (business_category_id) +); +create table category +( + category_id bigint generated by default as identity, + created_at timestamp(6), + updated_at timestamp(6), + category_name varchar(255), + primary key (category_id) +); +create table chat +( + chat_deleted BOOLEAN DEFAULT false, + account_unique_id bigint, + chat_id bigint generated by default as identity, + chat_register_date timestamp(6), + chat_room_id bigint, + chat_content varchar(255), + primary key (chat_id) +); + +create table chat_room +( + chat_room_id bigint generated by default as identity, + suggest_generated_date timestamp(6), + suggest_id bigint unique, + primary key (chat_room_id) +); + +create table contract +( + contract_hourly_pay integer, + contract_succeded boolean DEFAULT false, + contract_end_time timestamp(6), + contract_id bigint generated by default as identity, + contract_start_time timestamp(6), + created_at timestamp(6), + suggest_id bigint unique, + updated_at timestamp(6), + primary key (contract_id) +); + + +create table employee +( + employee_star_rating float(24), + employee_work_count integer, + account_id bigint unique, + created_at timestamp(6), + employee_id bigint generated by default as identity, + updated_at timestamp(6), + employee_nickname varchar(255), + primary key (employee_id) +); + +create table employer +( + account_id bigint unique, + created_at timestamp(6), + employer_id bigint generated by default as identity, + updated_at timestamp(6), + employer_nickname varchar(255), + primary key (employer_id) +); + +create table exteranl_carrer +( + created_at timestamp(6), + employee_id bigint, + id bigint generated by default as identity, + updated_at timestamp(6), + business_name varchar(255), + part_time_period varchar(255), + primary key (id) +); + +create table flavored +( + category_id bigint, + created_at timestamp(6), + employee_id bigint, + flavored_id bigint generated by default as identity, + updated_at timestamp(6), + primary key (flavored_id), + unique (employee_id, category_id) +); + +create table offer_employment +( + suggest_hourly_pay integer, + suggest_readed boolean DEFAULT false, + suggest_succeded boolean DEFAULT false, + business_id bigint unique, + employee_id bigint unique, + suggest_end_time timestamp(6), + suggest_id bigint generated by default as identity, + suggest_register_time timestamp(6), + suggest_start_time timestamp(6), + primary key (suggest_id) +); + +create table possible_board +( + created_at timestamp(6), + employee_id bigint, + possible_end_time timestamp(6), + possible_id bigint generated by default as identity, + possible_start_time timestamp(6), + updated_at timestamp(6), + primary key (possible_id) +); + +create table review +( + review_star_point integer, + created_at timestamp(6), + suggest_id bigint generated by default as identity, + updated_at timestamp(6), + review_content varchar(255), + primary key (suggest_id) +); + + +create table scrap +( + created_at timestamp(6), + employee_employee_id bigint, + employer_employer_id bigint, + scrap_id bigint generated by default as identity, + updated_at timestamp(6), + primary key (scrap_id) +); + +alter table if exists business + add constraint FKrmvi7a8hp1gvo4fs2vifp1u8s + foreign key (business_employer_id) + references employer; + + +alter table if exists business_category + add constraint FKpphxqd3m7af7xsylvpb26gqp8 + foreign key (business_id) + references business; + +alter table if exists business_category + add constraint FKh2g43n4c0mukqvpami4x0st36 + foreign key (categorey_id) + references category; + +alter table if exists chat + add constraint FKijoxkthso6n3r8bian3xu8dau + foreign key (account_unique_id) + references account; + +alter table if exists chat + add constraint FK44b6elhh512d2722l09i6qdku + foreign key (chat_room_id) + references chat_room; + +alter table if exists chat_room + add constraint FKs40aoy58klwk8k66k90vpvxek + foreign key (suggest_id) + references offer_employment; + +alter table if exists contract + add constraint FKcvp9vmfpl0bm6b6c63l2ormj6 + foreign key (suggest_id) + references offer_employment; + +alter table if exists employee + add constraint FKcfg6ajo8oske94exynxpf7tf9 + foreign key (account_id) + references account; + +alter table if exists employer + add constraint FKj4pj26t1ecnkh85f5dehhvj95 + foreign key (account_id) + references account; + +alter table if exists exteranl_carrer + add constraint FK47yjhdvwno2buwixnbbiipuvs + foreign key (employee_id) + references employee; + +alter table if exists flavored + add constraint FK5fko12aoyvyiogwi3bxr7p5pf + foreign key (category_id) + references category; + +alter table if exists flavored + add constraint FKgp8oorhip9jq4ulnlj3qo6tqj + foreign key (employee_id) + references employee; + +alter table if exists offer_employment + add constraint FK8xi6px87at3h94jr4434dxt6h + foreign key (business_id) + references business; + +alter table if exists offer_employment + add constraint FK8d521thyf88sosfv3kdni94cc + foreign key (employee_id) + references employee; + +alter table if exists possible_board + add constraint FKpo0s6j32cqd4dqtregricmak0 + foreign key (employee_id) + references employee; + +alter table if exists scrap + add constraint FKqml51wgiigjp3arbctk13hk1x + foreign key (employee_employee_id) + references employee; + +alter table if exists scrap + add constraint FK7atea93v4sptxh64isjnikhgm + foreign key (employer_employer_id) + references employer; \ No newline at end of file diff --git a/src/test/resources/test-files/test-image.png b/src/test/resources/test-files/test-image.png new file mode 100644 index 00000000..a22f0faa Binary files /dev/null and b/src/test/resources/test-files/test-image.png differ