Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
739077d
[#2] feat: 레디스 설정
Kguswo Mar 30, 2025
17eeac5
[#2] feat: application.yml 작성
Kguswo Mar 30, 2025
ab9737a
Merge pull request #3 from Kguswo/feature/initial-settings
Kguswo Mar 30, 2025
2607be6
[#5] feat: hits badge generator util class implement
Kguswo Apr 2, 2025
a052aed
[#5] feat: implement HitService
Kguswo Apr 2, 2025
ef2f19d
[#5] feat: ApiResponse 클래스 구현
Kguswo Apr 2, 2025
ab6c533
[#5] feat: 전역 예외 처리기 생성
Kguswo Apr 2, 2025
eec3df9
[#5] feat: 확장 함수 추가
Kguswo Apr 2, 2025
6de263d
[#5] feat: ApiResponse 패턴 적용한 컨트롤러 구현
Kguswo Apr 2, 2025
ef325b1
Update issue templates
Kguswo Apr 2, 2025
2df47cb
Create issue_template.yml
Kguswo Apr 2, 2025
f3583dc
Update issue templates
Kguswo Apr 2, 2025
fcd265b
Merge pull request #6 from Kguswo/feature/badge-generator
Kguswo Apr 2, 2025
742f946
[#7] feat: 웹 인터페이스용 컨트롤러 구현
Kguswo Apr 2, 2025
e0d1baa
[#7] feat: 단일 페이지 index.html 작성
Kguswo Apr 2, 2025
614aa17
[#7] feat: css 스타일 추가
Kguswo Apr 2, 2025
9e5ae5f
Merge pull request #8 from Kguswo/feature/web-interface
Kguswo Apr 2, 2025
1d72a54
[#9] feat: mockk 의존성 추가
Kguswo Apr 2, 2025
9561191
[#9] fix: redisTemplate 타입 <Any, Any>에서 <String, Long>로 일치하도록 변경
Kguswo Apr 2, 2025
aa66c8d
[#9] fix: application.yml 레디스 설정
Kguswo Apr 2, 2025
2c4cf93
[#9] test: SvgGeneratorTest 작성
Kguswo Apr 2, 2025
3a8c464
[#9] test: HitsServiceTest 작성
Kguswo Apr 2, 2025
e1b289b
[#9] test: HitsControllerTest 작성
Kguswo Apr 2, 2025
0975890
[#9] test: WebControllerTest 작성
Kguswo Apr 2, 2025
b12a784
[#9] test: 통합테스트 작성
Kguswo Apr 2, 2025
c8ca719
Merge pull request #10 from Kguswo/test/tdd-implement
Kguswo Apr 2, 2025
5bda806
Update README.md
Kguswo Apr 2, 2025
4e958cf
Update README_KR.md
Kguswo Apr 2, 2025
e551085
[#11] fix: redis 연동 해제
Kguswo Apr 3, 2025
9d7c108
[#11] feat: firebase 의존성 추가
Kguswo Apr 3, 2025
f30ec1d
[#11] feat: firebase 연결 설정
Kguswo Apr 3, 2025
301dfb6
[#11] feat: firestore db 사용 서비스 구현
Kguswo Apr 3, 2025
dbbce75
[#11] fix: redis 연결 임시 해제
Kguswo Apr 3, 2025
24fd818
[#11] fix: 웹 페이지 수정
Kguswo Apr 3, 2025
04ff4ca
[#11] feat: 미리보기 구현
Kguswo Apr 3, 2025
9fe62cc
[#11] fix: 미사용 서비스 임시 해제
Kguswo Apr 3, 2025
a0a7628
[#11] feat: 미리보기 추가 컨트롤러
Kguswo Apr 3, 2025
259e7c7
Merge pull request #12 from Kguswo/feature/firebase-linking
Kguswo Apr 3, 2025
24e829a
Update README.md
Kguswo Apr 3, 2025
123a56d
Update README_KR.md
Kguswo Apr 3, 2025
5c6491e
docs: .gitignore firebase 키 정보 json파일 추가
Kguswo Apr 3, 2025
bd8ede3
fix: google cloud run 설정
Kguswo Apr 3, 2025
b06c617
Merge pull request #13 from Kguswo/feature/google-cloud-run
Kguswo Apr 3, 2025
b8a2808
fix: 배포용 오류 수정
Kguswo Apr 3, 2025
206ecb7
docs: gcloudignore 작성
Kguswo Apr 3, 2025
1942508
feat: Dockerfile작성
Kguswo Apr 3, 2025
a699b4a
Merge pull request #14 from Kguswo/feature/google-cloud-run
Kguswo Apr 3, 2025
57a339e
fix: cors 에러 해결 및 db 추가시점 변경
Kguswo Apr 3, 2025
89c044d
Merge pull request #15 from Kguswo/fix/db-updating
Kguswo Apr 3, 2025
edc89bb
fix: cloudbuild.yml 추가
Kguswo Apr 3, 2025
cc58c06
fix: cloudbuild.yml, dockerfile 디렉토리 루트로 변경
Kguswo Apr 3, 2025
c58e74d
fix: 포드 명시적으로 수정
Kguswo Apr 3, 2025
fef94c8
fix: 배포를 위한 업데이트
Kguswo Apr 3, 2025
13d3909
fix: 배포를 위한 업데이트
Kguswo Apr 3, 2025
f61d1fa
fix: 배포를 위한 업데이트
Kguswo Apr 3, 2025
db9d10c
fix: 배포를 위한 Dockerfile 및 cloudbuild 파일명 yml에서 yaml로 변경
Kguswo Apr 4, 2025
396b314
fix: cloudbuild.yaml secret 마운트
Kguswo Apr 4, 2025
ce49112
fix: cloudbuild.yaml 수정
Kguswo Apr 4, 2025
4efd526
fix: Firebase 설정 코드에서 Base64 디코딩 제거
Kguswo Apr 4, 2025
eaba17b
fix: 포트 설정
Kguswo Apr 4, 2025
e4da334
fix: 베포 오류 수정
Kguswo Apr 4, 2025
0cea3c4
Update README.md
Kguswo Apr 4, 2025
741191e
Update README_KR.md
Kguswo Apr 4, 2025
26a6d5b
fix: 리전 도쿄로 변경 (커스텀 도메인 위함)
Kguswo Apr 4, 2025
5d9d0c5
Merge branch 'develop' of https://github.com/Kguswo/visitor_counter i…
Kguswo Apr 4, 2025
c1b3bf9
fix: 리전 도쿄로 변경 (커스텀 도메인 위함)
Kguswo Apr 4, 2025
65fc159
fix: modify index.html
Kguswo Apr 4, 2025
05a20be
fix: index.html, sytle.css 수정
Kguswo Apr 5, 2025
7d87e03
fix: index.html, sytle.css 수정
Kguswo Apr 5, 2025
b083983
Update README.md
Kguswo Apr 5, 2025
bf6e60b
Update README_KR.md
Kguswo Apr 5, 2025
7836055
Update README.md
Kguswo Apr 5, 2025
1367f0b
Update README_KR.md
Kguswo Apr 5, 2025
83f57f7
Update README.md
Kguswo Apr 5, 2025
3de0001
Update README_KR.md
Kguswo Apr 5, 2025
c8f5a74
fix: 방문자 배지 관련 수정
Kguswo Jun 15, 2025
f201632
test: trigger cloud build
Kguswo Jun 15, 2025
f5855aa
test: trigger cloud build
Kguswo Jun 15, 2025
bd29633
test: trigger cloud build
Kguswo Jun 15, 2025
77bb4ed
test: trigger cloud build
Kguswo Jun 15, 2025
804622d
test: trigger cloud build
Kguswo Jun 15, 2025
c0d86fe
feat: Add warmer scheduling
Kguswo Sep 6, 2025
3dd297a
feat: Add Firebase Analytics
Kguswo Sep 6, 2025
e073511
feat: Add Analytics for server
Kguswo Sep 6, 2025
e5c3998
feat: Add Analytics for server
Kguswo Sep 6, 2025
319d497
feat: Add Analytics for server
Kguswo Sep 6, 2025
3b36090
feat: Add Analytics for server
Kguswo Sep 6, 2025
5c56cf2
fix: Update warmer period to 5min
Kguswo Sep 7, 2025
13c0989
chore: Update semi title
Kguswo Sep 7, 2025
5a0942e
Update visitor badge image in README
Kguswo Sep 7, 2025
2f11bc0
Change image link in README_KR.md
Kguswo Sep 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/ISSUE_TEMPLATE/issue_template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: "Issue"
description: "이슈 추가"
body:
- type: textarea
attributes:
label: 📄 설명
description: 새로운 기능에 대한 설명을 작성해 주세요.
placeholder: 자세히 적을수록 좋습니다!
validations:
required: true
- type: textarea
attributes:
label: ✅ 작업할 내용
description: 할 일을 체크박스 형태로 작성해주세요.
placeholder: 최대한 세분화 해서 적어주세요!
validations:
required: true
- type: textarea
attributes:
label: 🙋🏻 참고 자료
description: 참고 자료가 있다면 작성해 주세요.
30 changes: 30 additions & 0 deletions .github/workflows/warmer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Keep Cloud Run Warm

on:
schedule:
- cron: '*/5 * * * *'
- cron: '2-59/5 * * * *'
workflow_dispatch:

jobs:
warm-service:
runs-on: ubuntu-latest
timeout-minutes: 3

steps:
- name: Primary ping
id: primary
run: |
if curl -f -s -o /dev/null --max-time 20 \
"https://hitmeup-backend-593087166771.asia-northeast1.run.app/api/ping"; then
echo "success=true" >> $GITHUB_OUTPUT
else
echo "success=false" >> $GITHUB_OUTPUT
fi

- name: Backup ping
if: steps.primary.outputs.success != 'true'
run: |
sleep 30
curl -f -s -o /dev/null --max-time 20 \
"https://hitmeup-backend-593087166771.asia-northeast1.run.app/api/ping"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,7 @@ gradle-app.setting
# Java heap dump
*.hprof

### secret-key
firebase-service-account.json

# End of https://www.toptal.com/developers/gitignore/api/java,kotlin,intellij,macos,gradle
35 changes: 35 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 1단계: 빌드 환경
FROM eclipse-temurin:21-jdk as builder
WORKDIR /app

# Gradle 파일 복사 (캐시 최적화)
COPY hitmeup_backend/gradlew /app/
COPY hitmeup_backend/gradle /app/gradle
COPY hitmeup_backend/build.gradle.kts /app/
COPY hitmeup_backend/settings.gradle.kts /app/
COPY hitmeup_backend/src /app/src

# 권한 설정
RUN chmod +x /app/gradlew

# 2단계: 빌드 실행
RUN ./gradlew build -x test --no-daemon

# 3단계: 실행 환경
FROM eclipse-temurin:21-jdk
WORKDIR /app

# 빌드 결과물만 복사
COPY --from=builder /app/build/libs/hitmeup_backend-0.0.1-SNAPSHOT.jar app.jar

# 환경 변수 및 포트 설정
ENV PORT=8080
ENV SPRING_PROFILES_ACTIVE=prod

# 디버깅을 위한 추가 환경 변수
# ENV JAVA_TOOL_OPTIONS="-XX:+PrintCommandLineFlags -verbose:class"

EXPOSE 8080

# 4단계: 실행 명령
ENTRYPOINT ["java", "-jar", "-Dserver.port=8080", "app.jar"]
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Hit Me Up ([한국어 ver.](README_KR.md))

### [Hit_Me_Up](https://hit-me-up-336ec.web.app/)
Create a badge that counts visitors to a specific URL. Generate the badge and add it to your README to track visitor counts.

![image](https://github.com/user-attachments/assets/dbc7b630-351d-4edd-adfa-5d4aa6cfff9f)
3 changes: 3 additions & 0 deletions README_KR.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Hit Me Up
### [Hit_Me_Up](https://hit-me-up-336ec.web.app/)

특정 URL의 방문자 수를 세는 뱃지를 만들어보세요. 뱃지를 생성하고 리드미에 추가하면 방문자 수를 추적할 수 있습니다.

![image](https://github.com/user-attachments/assets/dbc7b630-351d-4edd-adfa-5d4aa6cfff9f)
27 changes: 27 additions & 0 deletions cloudbuild.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
steps:
# Docker 빌드 단계
- name: 'gcr.io/cloud-builders/docker'
args: [ 'build', '-t', 'gcr.io/hit-me-up-336ec/hitmeup:latest', '.' ]

# Docker 이미지 푸시 단계
- name: 'gcr.io/cloud-builders/docker'
args: [ 'push', 'gcr.io/hit-me-up-336ec/hitmeup:latest' ]

# Cloud Run 배포 단계
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: gcloud
args:
- 'run'
- 'deploy'
- 'hitmeup-backend'
- '--image=gcr.io/hit-me-up-336ec/hitmeup:latest'
- '--region=asia-northeast1'
- '--platform=managed'
- '--allow-unauthenticated'
- '--set-env-vars=SPRING_PROFILES_ACTIVE=prod'
- '--set-env-vars=APP_DOMAIN=https://hitmeup-backend-593087166771.asia-northeast1.run.app'
- '--set-secrets=FIREBASE_CONFIG=firebase-service-account:latest'
- '--timeout=300s'

images:
- 'gcr.io/hit-me-up-336ec/hitmeup:latest'
2 changes: 2 additions & 0 deletions hitmeup_backend/.gcloudignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# 기본적으로 모든 파일 포함
!*
50 changes: 28 additions & 22 deletions hitmeup_backend/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,42 +1,48 @@
plugins {
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
id("org.springframework.boot") version "3.4.4"
id("io.spring.dependency-management") version "1.1.7"
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
id("org.springframework.boot") version "3.4.4"
id("io.spring.dependency-management") version "1.1.7"
}

group = "com.hitmeup"
version = "0.0.1-SNAPSHOT"

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}

repositories {
mavenCentral()
mavenCentral()
}

val mockkVersion = "1.13.5"

dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
developmentOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
developmentOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("io.mockk:mockk:${mockkVersion}")
// firebase
implementation("com.google.firebase:firebase-admin:9.2.0")

testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
}

tasks.withType<Test> {
useJUnitPlatform()
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.hitmeup.backend.config

import com.google.auth.oauth2.GoogleCredentials
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.ClassPathResource
import java.io.ByteArrayInputStream
import java.io.IOException
import java.util.*


@Configuration
class FirebaseConfig {
private val logger = LoggerFactory.getLogger(FirebaseConfig::class.java)

@Bean
@Throws(IOException::class)
fun firebaseApp(): FirebaseApp {
try {
// 시스템 환경 변수 로깅
logger.info("시스템 환경 변수 목록:")
System.getenv().forEach { (key, value) ->
if (!key.contains("secret", ignoreCase = true) &&
!key.contains("password", ignoreCase = true) &&
!key.contains("firebase", ignoreCase = true)
) {
logger.info("$key = $value")
} else {
logger.info("$key = [보안 값]")
}
}

// 이미 초기화된 앱 있는지 체크
val existingApps = FirebaseApp.getApps()
if (existingApps.isNotEmpty()) {
logger.info("기존 Firebase 앱을 사용합니다.")
return FirebaseApp.getInstance()
}

logger.info("Firebase 앱 초기화 시도 중...")

// 환경 변수에서 Firebase 설정 가져오기 시도
val firebaseConfigEnv = System.getenv("FIREBASE_CONFIG")

if (firebaseConfigEnv != null) {
logger.info("FIREBASE_CONFIG 환경 변수가 존재합니다.")
try {
// 환경 변수의 내용이 유효한 JSON인지 간단히 확인
if (firebaseConfigEnv.trim().startsWith("{") && firebaseConfigEnv.trim().endsWith("}")) {
logger.info("Firebase 구성이 JSON 형식으로 보입니다.")
} else {
logger.warn("Firebase 구성이 JSON 형식이 아닐 수 있습니다.")
}

val credentials =
GoogleCredentials.fromStream(ByteArrayInputStream(firebaseConfigEnv.toByteArray()))
val options = FirebaseOptions.builder()
.setCredentials(credentials)
.build()

val app = FirebaseApp.initializeApp(options)
logger.info("Firebase 앱 초기화 성공: ${app.name}")
return app
} catch (e: Exception) {
logger.error("환경 변수에서 Firebase 초기화 실패: ${e.message}", e)
// 환경 변수 방식이 실패하면 파일 방식으로 폴백
logger.info("파일 방식으로 초기화 시도")
}
} else {
logger.warn("FIREBASE_CONFIG 환경 변수가 없습니다.")
}

// 로컬 파일에서 로드 (개발 환경용)
logger.info("로컬 파일에서 Firebase 설정을 로드합니다.")
val resource = ClassPathResource("firebase-service-account.json")
if (!resource.exists()) {
logger.error("Firebase 서비스 계정 파일을 찾을 수 없습니다: firebase-service-account.json")
throw IOException("Firebase 서비스 계정 파일을 찾을 수 없습니다.")
}

val credentials = GoogleCredentials.fromStream(resource.inputStream)
val options = FirebaseOptions.builder()
.setCredentials(credentials)
.build()

val app = FirebaseApp.initializeApp(options)
logger.info("Firebase 앱 초기화 성공: ${app.name}")
return app
} catch (e: Exception) {
logger.error("Firebase 앱 초기화 중 오류 발생: ${e.message}", e)
throw e
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.hitmeup.backend.config

//import org.springframework.context.annotation.Bean
//import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.StringRedisSerializer

//@Configuration
class RedisConfig {

// @Bean
fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate<String, Long> {
val template = RedisTemplate<String, Long>()
template.setConnectionFactory(connectionFactory)
template.keySerializer = StringRedisSerializer()
template.valueSerializer = StringRedisSerializer()
return template
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.hitmeup.backend.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import org.springframework.web.filter.CorsFilter

@Configuration
class WebConfig {

@Bean
fun corsFilter(): CorsFilter {

val source = UrlBasedCorsConfigurationSource()
val config = CorsConfiguration()

config.allowedOrigins = listOf("*")
config.addAllowedHeader("*")
config.addAllowedMethod("*")

source.registerCorsConfiguration("/**", config)
return CorsFilter(source)
}
}
Loading