Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
on:
issue_comment:
types: [ created ]

name: "Deploy to Dev Cloud Run"

env:
PROJECT_ID: mayb-api-458206
GAR_LOCATION: asia-northeast3
REPOSITORY: mayb-repo
IMAGE_NAME: mayb-api
REGION: asia-northeast1
DEV_SERVICE_NAME: mayb-api-dev

jobs:
deploy-dev:
runs-on: ubuntu-latest
if: contains(github.event.comment.body, '/dev-release')

permissions:
contents: 'read'
id-token: 'write'
pull-requests: 'write'

steps:
- name: 'Get PR details'
id: pr
uses: actions/github-script@v6
with:
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
core.setOutput('ref', pr.head.ref);
core.setOutput('sha', pr.head.sha);
core.setOutput('title', pr.title);
core.setOutput('html_url', pr.html_url);

- uses: 'actions/checkout@v4'
with:
ref: ${{ steps.pr.outputs.sha }}

- uses: 'google-github-actions/auth@v2'
with:
credentials_json: ${{ secrets.GCP_DEPLOY_SA_KEY }}

- uses: 'google-github-actions/setup-gcloud@v2'
with:
project_id: '${{ env.PROJECT_ID }}'

- name: 'Set variables'
run: |-
echo "IMAGE=${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.pr.outputs.sha }}" >> $GITHUB_ENV

- uses: 'actions/setup-java@v3'
with:
distribution: temurin
java-version: 21

- uses: gradle/gradle-build-action@v2
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Execute Gradle build
run: ./gradlew build

- name: 'Docker auth'
run: |-
gcloud auth configure-docker ${{ env.GAR_LOCATION }}-docker.pkg.dev

- name: Build and Push Container
run: |-
docker build --file ./Dockerfile -t "${{ env.IMAGE }}" .
docker push "${{ env.IMAGE }}"

- name: Create dev manifest
run: |-
export IMAGE="${{ env.IMAGE }}"
export SERVICE="${{ env.DEV_SERVICE_NAME }}"
export DB_URL="${{ secrets.DEV_DB_URL }}"
export DB_USERNAME="${{ secrets.DEV_DB_USERNAME }}"
export DB_PASSWORD="${{ secrets.DEV_DB_PASSWORD }}"
export ENCRYPTION_SECRET_KEY="${{ secrets.DEV_ENCRYPTION_SECRET_KEY }}"
export JWT_SECRET_ACCESS="${{ secrets.DEV_JWT_SECRET_ACCESS }}"
export JWT_SECRET_REFRESH="${{ secrets.DEV_JWT_SECRET_REFRESH }}"
envsubst < ./deploy/run/dev.template.yaml > dev.yaml

- name: Deploy to Cloud Run
id: deploy
uses: google-github-actions/deploy-cloudrun@v1
with:
region: ${{ env.REGION }}
metadata: dev.yaml

- name: Set no-auth policy
run: |-
cat <<EOF > policy.yaml
bindings:
- members:
- allUsers
role: roles/run.invoker
EOF
gcloud run services set-iam-policy "${{ env.DEV_SERVICE_NAME }}" policy.yaml --region ${{ env.REGION }} --quiet

- name: 'Comment URL'
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '🚀 Dev deploy complete: ${{ steps.deploy.outputs.url }}\n📦 Image: `${{ env.IMAGE }}`\n📝 Branch: `${{ steps.pr.outputs.ref }}`\n💾 Commit: ${{ steps.pr.outputs.sha }}'
})

- name: 'Create slack payload'
run: |-
export PR_NAME="${{ steps.pr.outputs.title }}"
export PR_LINK="${{ steps.pr.outputs.html_url }}"
export SERVICE_URL="${{ steps.deploy.outputs.url }}"
export REPO_NAME="${{ github.event.repository.name }}"
export REPO_LINK="${{ github.event.repository.html_url }}"
export BRANCH_NAME="${{ steps.pr.outputs.ref }}"

COMMIT=${{ steps.pr.outputs.sha }}
SHORT_SHA=${COMMIT::7}
export COMMIT_SHA="${SHORT_SHA}"

cat <<EOF > dev-deploy-payload.json
{
"text": "🚀 Dev Environment Deployed - ${REPO_NAME} (${BRANCH_NAME})",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "🚀 *Dev Environment Deployed*"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Repository:*\n<${REPO_LINK}|${REPO_NAME}>"
},
{
"type": "mrkdwn",
"text": "*Branch:*\n${BRANCH_NAME}"
},
{
"type": "mrkdwn",
"text": "*PR:*\n<${PR_LINK}|${PR_NAME}>"
},
{
"type": "mrkdwn",
"text": "*Commit:*\n${COMMIT_SHA}"
},
{
"type": "mrkdwn",
"text": "*Service URL:*\n<${SERVICE_URL}|Open Dev Service>"
}
]
}
]
}
EOF

- uses: slackapi/slack-github-action@v1.24.0
with:
channel-id: ${{ vars.SLACK_CHANNEL_ID }}
payload-file-path: ./dev-deploy-payload.json
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
40 changes: 40 additions & 0 deletions deploy/run/dev.template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: ${SERVICE}
annotations:
run.googleapis.com/ingress: all
run.googleapis.com/ingress-status: all
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/minScale: '0'
autoscaling.knative.dev/maxScale: '1'
run.googleapis.com/startup-cpu-boost: 'true'
spec:
containers:
- env:
- name: SPRING_PROFILES_ACTIVE
value: dev
- name: DB_URL
value: ${DB_URL}
- name: DB_USERNAME
value: ${DB_USERNAME}
- name: DB_PASSWORD
value: ${DB_PASSWORD}
- name: ENCRYPTION_SECRET_KEY
value: ${ENCRYPTION_SECRET_KEY}
- name: JWT_SECRET_ACCESS
value: ${JWT_SECRET_ACCESS}
- name: JWT_SECRET_REFRESH
value: ${JWT_SECRET_REFRESH}
image: ${IMAGE}
ports:
- containerPort: 8080
name: http1
resources:
limits:
cpu: 1000m
memory: 1Gi

1 change: 1 addition & 0 deletions deploy/slack/feature-deploy-payload.template.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"text": "${PR_NAME} deployed successfully - ${BRANCH_NAME}",
"username": "API Feature Deploy",
"icon_emoji": ":lightning_cloud:",
"blocks": [
Expand Down
101 changes: 101 additions & 0 deletions src/main/java/kr/mayb/controller/QnAController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package kr.mayb.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import kr.mayb.dto.QnADto;
import kr.mayb.enums.QnAStatus;
import kr.mayb.facade.QnAFacade;
import kr.mayb.security.DenyAll;
import kr.mayb.security.PermitAdmin;
import kr.mayb.security.PermitAll;
import kr.mayb.security.PermitAuthenticated;
import kr.mayb.util.request.PageRequest;
import kr.mayb.util.response.ApiResponse;
import kr.mayb.util.response.PageResponse;
import kr.mayb.util.response.Responses;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Tag(name = "QnA", description = "QnA API")
@DenyAll
@RestController
@RequiredArgsConstructor
public class QnAController {

private final QnAFacade qnAFacade;

@Operation(summary = "상품 QnA 등록")
@PermitAuthenticated
@PostMapping("/questions")
public ResponseEntity<ApiResponse<QnADto>> registerQuestion(@RequestBody @Valid QuestionRequest request) {
QnADto response = qnAFacade.registerQuestion(request.productId(), request.question(), request.isSecret());
return Responses.ok(response);
}

@Operation(summary = "상품 QnA 답변 등록")
@PermitAdmin
@PostMapping("/questions/answers")
public ResponseEntity<ApiResponse<QnADto>> registerAnswer(@RequestBody @Valid AnswerRequest request) {
QnADto response = qnAFacade.registerAnswer(request.questionId(), request.answer());
return Responses.ok(response);
}

@Operation(summary = "상품 QnA 조회")
@PermitAll
@GetMapping("/questions")
public ResponseEntity<ApiResponse<PageResponse<QnADto, Void>>> getQnAs(@RequestParam("pid") long productId,
@RequestParam("ex_secret") boolean excludeSecret,
@RequestParam("only_mine") boolean onlyMine,
@RequestParam("status") QnAStatus status,
PageRequest pageRequest) {
PageResponse<QnADto, Void> response = qnAFacade.getQnAs(productId, excludeSecret, onlyMine, status, pageRequest);
return Responses.ok(response);
}

@Operation(summary = "상품 질문 수정")
@PermitAuthenticated
@PatchMapping("/questions/{questionId}")
public ResponseEntity<ApiResponse<QnADto>> updateQuestion(@PathVariable long questionId, @RequestBody @Valid UpdateRequest request) {
QnADto response = qnAFacade.updateQuestion(questionId, request.content());
return Responses.ok(response);
}

@Operation(summary = "상품 답변 수정")
@PermitAdmin
@PatchMapping("/questions/{questionId}/answers")
public ResponseEntity<ApiResponse<QnADto>> updateAnswer(@PathVariable long questionId, @RequestBody @Valid UpdateRequest request) {
QnADto response = qnAFacade.updateAnswer(questionId, request.content());
return Responses.ok(response);
}

@Operation(summary = "상품 질문 삭제")
@PermitAuthenticated
@DeleteMapping("/questions/{questionId}")
public ResponseEntity<Void> removeQuestion(@PathVariable long questionId) {
qnAFacade.removeQuestion(questionId);
return Responses.noContent();
}

private record QuestionRequest(
long productId,
@NotBlank
String question,
boolean isSecret
) {
}

private record AnswerRequest(
long questionId,
@NotBlank
String answer
) {
}

private record UpdateRequest(
String content
) {
}
}
4 changes: 1 addition & 3 deletions src/main/java/kr/mayb/data/model/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,6 @@ public String getMaskedName() {
}

String firstChar = this.name.substring(0, 1);
String masked = this.name.substring(1).replaceAll("\\.", "*");

return firstChar + masked;
return firstChar + "****";
}
}
31 changes: 31 additions & 0 deletions src/main/java/kr/mayb/data/model/UserQuestion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package kr.mayb.data.model;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Table(schema = "mayb")
@Entity
public class UserQuestion extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

@Column(nullable = false)
private String question;

@Column
private String answer;

@Column(nullable = false)
private boolean isSecret;

@Column(nullable = false)
private long productId;

@Column(nullable = false)
private long memberId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package kr.mayb.data.repository;

import kr.mayb.data.model.UserQuestion;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface UserQuestionRepository extends JpaRepository<UserQuestion, Long>, JpaSpecificationExecutor<UserQuestion> {
}
Loading
Loading