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
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: CI

on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
backend-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend/tiggle-root
steps:
- uses: actions/checkout@v4

- name: Setup JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: true

- name: Test
run: ./gradlew :tiggle:test

frontend-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend/tiggle
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: frontend/tiggle/package-lock.json

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Test
run: npm test

- name: Build
run: npm run build
60 changes: 57 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,78 @@ name: Deploy to NAS Server

on:
push:
branches: [ main ]
branches: [main]

permissions:
contents: read

jobs:
# ── Gate: Backend tests must pass before deploy ──
test-backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend/tiggle-root
steps:
- uses: actions/checkout@v4

- name: Setup JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3

- name: Test
run: ./gradlew :tiggle:test

# ── Gate: Frontend tests must pass before deploy ──
test-frontend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend/tiggle
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: frontend/tiggle/package-lock.json

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Test
run: npm test

- name: Build
run: npm run build

# ── Deploy (only after all tests pass) ──
deploy:
name: Deploy
needs: [test-backend, test-frontend]
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: ssh connect
- name: SSH Deploy to NAS
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: ${{ secrets.PORT }}
command_timeout: 15m
script: |
bash -l -c "
cd Tiggle
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.side.tiggle.domain.achievement.api

import com.side.tiggle.domain.achievement.dto.resp.AchievementRespDto
import com.side.tiggle.domain.achievement.service.AchievementService
import com.side.tiggle.global.common.ApiResponse
import com.side.tiggle.global.common.constants.HttpHeaders
import org.springframework.http.ResponseEntity
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*

@Validated
@RestController
@RequestMapping("/api/v1/achievements")
class AchievementApiController(
private val achievementService: AchievementService
) {

@GetMapping
fun getAllAchievements(
@RequestHeader(name = HttpHeaders.MEMBER_ID) memberId: Long
): ResponseEntity<ApiResponse<List<AchievementRespDto>>> {
val achievements = achievementService.getAllAchievements(memberId)
return ResponseEntity.ok(ApiResponse.success(achievements))
}

@GetMapping("/recent")
fun getRecentAchievements(
@RequestHeader(name = HttpHeaders.MEMBER_ID) memberId: Long,
@RequestParam(defaultValue = "5") limit: Int
): ResponseEntity<ApiResponse<List<AchievementRespDto>>> {
val achievements = achievementService.getRecentAchievements(memberId, limit)
return ResponseEntity.ok(ApiResponse.success(achievements))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.side.tiggle.domain.achievement.dto.resp

import com.side.tiggle.domain.achievement.model.AchievementConditionType
import java.time.LocalDateTime

data class AchievementRespDto(
val id: Long,
val code: String,
val name: String,
val description: String?,
val conditionType: AchievementConditionType,
val conditionValue: Int,
val achieved: Boolean,
val achievedAt: LocalDateTime?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.side.tiggle.domain.achievement.exception

import com.side.tiggle.global.exception.CustomException
import com.side.tiggle.global.exception.error.ErrorCode

class AchievementException : CustomException {

private val errorCode: ErrorCode

constructor(errorCode: ErrorCode) : super(errorCode) {
this.errorCode = errorCode
}

constructor(errorCode: ErrorCode, cause: Throwable) : super(errorCode, cause) {
this.errorCode = errorCode
}

override fun getErrorCode(): ErrorCode = errorCode
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.side.tiggle.domain.achievement.exception.error

import com.side.tiggle.global.exception.error.ErrorCode
import org.springframework.http.HttpStatus

/**
* 업적(Achievement) 도메인 에러 코드 정의
*
* DD=86 (Achievement 도메인)
*/
enum class AchievementErrorCode(
private val status: HttpStatus,
private val code: Int,
private val msg: String
) : ErrorCode {
// 조회 관련 오류 (86001~86010)
ACHIEVEMENT_NOT_FOUND(HttpStatus.NOT_FOUND, 86001, "업적을 찾을 수 없습니다"),

// 상태 관련 오류 (86011~86020)
ACHIEVEMENT_ALREADY_ACHIEVED(HttpStatus.CONFLICT, 86011, "이미 달성한 업적입니다");

override fun httpStatus(): HttpStatus = status
override fun codeNumber(): Int = code
override fun message(): String = msg
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.side.tiggle.domain.achievement.model

import jakarta.persistence.*

@Entity
@Table(name = "achievements")
class Achievement(
@Column(nullable = false, unique = true, length = 50)
val code: String,

@Column(nullable = false, length = 100)
val name: String,

@Column(length = 500)
val description: String? = null,

@Enumerated(EnumType.STRING)
@Column(name = "condition_type", nullable = false, length = 30)
val conditionType: AchievementConditionType,

@Column(name = "condition_value", nullable = false)
val conditionValue: Int,

@Column(name = "reward_item_id")
val rewardItemId: Long? = null,

@Column(name = "reward_exp", nullable = false)
val rewardExp: Int = 0
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.side.tiggle.domain.achievement.model

enum class AchievementConditionType {
RECORD_COUNT,
STREAK,
CHALLENGE_COMPLETE,
CATEGORY_COUNT,
SPENDING_DECREASE,
NO_ANOMALY_WEEKS,
NO_SPEND_DAYS,
COLOR_RARITY,
CHARACTER_TIER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.side.tiggle.domain.achievement.model

import jakarta.persistence.*
import java.time.LocalDateTime

@Entity
@Table(
name = "member_achievements",
uniqueConstraints = [UniqueConstraint(columnNames = ["member_id", "achievement_id"])]
)
class MemberAchievement(
@Column(name = "member_id", nullable = false)
val memberId: Long,

@Column(name = "achievement_id", nullable = false)
val achievementId: Long,

@Column(name = "achieved_at", nullable = false)
val achievedAt: LocalDateTime = LocalDateTime.now()
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.side.tiggle.domain.achievement.repository

import com.side.tiggle.domain.achievement.model.Achievement
import com.side.tiggle.domain.achievement.model.AchievementConditionType
import org.springframework.data.jpa.repository.JpaRepository

interface AchievementRepository : JpaRepository<Achievement, Long> {
fun findByCode(code: String): Achievement?
fun findByConditionType(conditionType: AchievementConditionType): List<Achievement>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.side.tiggle.domain.achievement.repository

import com.side.tiggle.domain.achievement.model.MemberAchievement
import org.springframework.data.jpa.repository.JpaRepository

interface MemberAchievementRepository : JpaRepository<MemberAchievement, Long> {
fun findAllByMemberId(memberId: Long): List<MemberAchievement>
fun existsByMemberIdAndAchievementId(memberId: Long, achievementId: Long): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.side.tiggle.domain.achievement.service

import com.side.tiggle.domain.achievement.dto.resp.AchievementRespDto
import com.side.tiggle.domain.achievement.model.AchievementConditionType

interface AchievementService {

fun getAllAchievements(memberId: Long): List<AchievementRespDto>

fun getRecentAchievements(memberId: Long, limit: Int = 5): List<AchievementRespDto>

fun checkAndGrantAchievements(
memberId: Long,
conditionType: AchievementConditionType,
currentValue: Int
)
}
Loading
Loading