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
39 changes: 39 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# ===========================================
# ProjectVG API Server - Environment Variables Template
# ===========================================
# Copy this file to .env and fill in your actual values

# Database Configuration
DB_CONNECTION_STRING=Server=host.docker.internal,1433;Database=ProjectVG;User Id=sa;Password=YOUR_DB_PASSWORD;TrustServerCertificate=true;MultipleActiveResultSets=true
DB_PASSWORD=YOUR_DB_PASSWORD

# Redis Configuration
REDIS_CONNECTION_STRING=host.docker.internal:6380

# External Services
LLM_BASE_URL=http://host.docker.internal:7908
MEMORY_BASE_URL=http://host.docker.internal:7912
TTS_BASE_URL=https://supertoneapi.com
TTS_API_KEY=YOUR_TTS_API_KEY

# JWT Configuration (IMPORTANT: Use a secure random key in production)
JWT_SECRET_KEY=YOUR_JWT_SECRET_KEY_MINIMUM_32_CHARACTERS
JWT_ACCESS_TOKEN_LIFETIME_MINUTES=15
JWT_REFRESH_TOKEN_LIFETIME_DAYS=30

# OAuth2 Configuration
OAUTH2_ENABLED=true
GOOGLE_OAUTH_ENABLED=true
GOOGLE_OAUTH_CLIENT_ID=YOUR_GOOGLE_CLIENT_ID
GOOGLE_OAUTH_CLIENT_SECRET=YOUR_GOOGLE_CLIENT_SECRET
GOOGLE_OAUTH_REDIRECT_URI=http://localhost:7900/auth/oauth2/callback
GOOGLE_OAUTH_AUTO_CREATE_USER=true
GOOGLE_OAUTH_DEFAULT_ROLE=User
Comment on lines +24 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

OAuth Redirect URI 포트 불일치로 콜백 실패 가능

docker-compose.yml은 호스트 7910→컨테이너 7900으로 포워딩합니다. 현재 GOOGLE_OAUTH_REDIRECT_URI는 7900을 가리켜 로컬 실행 시 OAuth 콜백이 실패할 수 있습니다. 7910으로 맞춰주세요.

-GOOGLE_OAUTH_REDIRECT_URI=http://localhost:7900/auth/oauth2/callback
+GOOGLE_OAUTH_REDIRECT_URI=http://localhost:7910/auth/oauth2/callback
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# OAuth2 Configuration
OAUTH2_ENABLED=true
GOOGLE_OAUTH_ENABLED=true
GOOGLE_OAUTH_CLIENT_ID=YOUR_GOOGLE_CLIENT_ID
GOOGLE_OAUTH_CLIENT_SECRET=YOUR_GOOGLE_CLIENT_SECRET
GOOGLE_OAUTH_REDIRECT_URI=http://localhost:7900/auth/oauth2/callback
GOOGLE_OAUTH_AUTO_CREATE_USER=true
GOOGLE_OAUTH_DEFAULT_ROLE=User
# OAuth2 Configuration
OAUTH2_ENABLED=true
GOOGLE_OAUTH_ENABLED=true
GOOGLE_OAUTH_CLIENT_ID=YOUR_GOOGLE_CLIENT_ID
GOOGLE_OAUTH_CLIENT_SECRET=YOUR_GOOGLE_CLIENT_SECRET
GOOGLE_OAUTH_REDIRECT_URI=http://localhost:7910/auth/oauth2/callback
GOOGLE_OAUTH_AUTO_CREATE_USER=true
GOOGLE_OAUTH_DEFAULT_ROLE=User
🧰 Tools
🪛 dotenv-linter (3.3.0)

[warning] 26-26: [UnorderedKey] The GOOGLE_OAUTH_ENABLED key should go before the OAUTH2_ENABLED key

(UnorderedKey)


[warning] 27-27: [UnorderedKey] The GOOGLE_OAUTH_CLIENT_ID key should go before the GOOGLE_OAUTH_ENABLED key

(UnorderedKey)


[warning] 28-28: [UnorderedKey] The GOOGLE_OAUTH_CLIENT_SECRET key should go before the GOOGLE_OAUTH_ENABLED key

(UnorderedKey)


[warning] 29-29: [UnorderedKey] The GOOGLE_OAUTH_REDIRECT_URI key should go before the OAUTH2_ENABLED key

(UnorderedKey)


[warning] 30-30: [UnorderedKey] The GOOGLE_OAUTH_AUTO_CREATE_USER key should go before the GOOGLE_OAUTH_CLIENT_ID key

(UnorderedKey)


[warning] 31-31: [UnorderedKey] The GOOGLE_OAUTH_DEFAULT_ROLE key should go before the GOOGLE_OAUTH_ENABLED key

(UnorderedKey)

🤖 Prompt for AI Agents
In .env.example around lines 24 to 31, the GOOGLE_OAUTH_REDIRECT_URI points to
port 7900 which conflicts with docker-compose host→container forwarding (host
7910 → container 7900) and can cause OAuth callback failures; update
GOOGLE_OAUTH_REDIRECT_URI to use http://localhost:7910/auth/oauth2/callback (or
make it configurable via env interpolation) so the host-facing port matches
docker-compose and OAuth callbacks succeed.


# Application Configuration
ASPNETCORE_ENVIRONMENT=Production

# Container Resource Limits
API_CPU_LIMIT=1.0
API_MEMORY_LIMIT=1g
API_PORT=7910
32 changes: 32 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Set default behavior to automatically normalize line endings.
* text=auto

# Set file types that should always have CRLF line endings on checkout.
*.sln text eol=crlf
*.csproj text eol=crlf
*.config text eol=crlf

# Set file types that should always have LF line endings on checkout.
*.sh text eol=lf
*.bash text eol=lf
*.yml text eol=lf
*.yaml text eol=lf

# Ensure that shell scripts are executable
*.sh text eol=lf
deploy.sh text eol=lf
deploy-dev.sh text eol=lf

# PowerShell scripts
*.ps1 text eol=crlf

# Docker files
Dockerfile text eol=lf
*.dockerfile text eol=lf
docker-compose*.yml text eol=lf

# Markdown files
*.md text eol=lf

# JSON files
*.json text eol=lf
72 changes: 72 additions & 0 deletions .github/workflows/develop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Develop

on:
pull_request:
branches: [develop]
paths-ignore:
- '**.md'
- 'docs/**'
- 'scripts/**'

env:
DOTNET_VERSION: '8.0.x'

jobs:
build-and-test:
name: Build & Test
runs-on: ubuntu-latest

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

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Cache NuGet
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-

- name: Restore
run: |
echo "복원 중..."
start_time=$(date +%s)
dotnet restore ProjectVG.sln
end_time=$(date +%s)
duration=$((end_time - start_time))
echo "복원 완료 (${duration}초)"

- name: Build
run: |
echo "🔨 빌드 중..."
start_time=$(date +%s)
dotnet build ProjectVG.sln --no-restore --configuration Release
end_time=$(date +%s)
duration=$((end_time - start_time))
echo "빌드 완료 (${duration}초)"

- name: Test
run: |
echo "테스트 중..."
start_time=$(date +%s)
dotnet test --no-build --configuration Release --verbosity normal
end_time=$(date +%s)
duration=$((end_time - start_time))
echo "테스트 완료 (${duration}초)"

- name: Success Status
if: success()
run: |
echo "✅ 빌드 및 테스트 성공"

- name: Build Status
if: failure()
run: |
echo "❌ 빌드 실패"
exit 1
121 changes: 121 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
name: Release

on:
push:
branches: [release]
paths-ignore:
- '**.md'
- 'docs/**'
- 'scripts/**'

env:
DOTNET_VERSION: '8.0.x'
DOCKER_IMAGE_NAME: ghcr.io/projectvg/projectvgapi
ACTOR: projectvg

permissions:
contents: read
packages: write

jobs:
build:
name: Build & Push
runs-on: ubuntu-latest

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

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Cache NuGet
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-

- name: Restore
run: |
echo "복원 중..."
dotnet restore ProjectVG.sln
echo "복원 완료"

- name: Build
run: |
echo "🔨 빌드 중..."
dotnet build ProjectVG.sln --no-restore --configuration Release
echo "빌드 완료"

- name: Test
run: |
echo "테스트 중..."
dotnet test --no-build --configuration Release --verbosity normal
echo "테스트 완료"

- name: Login GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ env.ACTOR }}
password: ${{ secrets.GHCR_TOKEN }}

- name: Build & Push Image
run: |
docker build -t ${{ env.DOCKER_IMAGE_NAME }}:latest -f ProjectVG.Api/Dockerfile .
docker push ${{ env.DOCKER_IMAGE_NAME }}:latest

- name: Build Success Status
if: success()
run: |
echo "✅ 빌드 및 이미지 푸시 완료"
echo "이미지: ${{ env.DOCKER_IMAGE_NAME }}:latest"

- name: Build Failure Status
if: failure()
run: |
echo "❌ 빌드 또는 이미지 푸시 실패"
exit 1

deploy:
name: Deploy
needs: build
runs-on: [self-hosted, deploy-runner]

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

- name: Login GHCR
run: echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u ${{ env.ACTOR }} --password-stdin

- name: Add Config Files
run: |
echo "${{ secrets.PROD_APPLICATION_ENV }}" | base64 --decode > .env

- name: Make Script Executable
run: chmod +x deploy/deploy.sh

- name: Deploy
run: ./deploy/deploy.sh

- name: Cleanup
run: |
docker image prune -f
docker builder prune -f

- name: Deploy Success Status
if: success()
run: |
echo "✅ 배포 완료"
echo "이미지: ${{ env.DOCKER_IMAGE_NAME }}:latest"

- name: Deploy Failure Status
if: failure()
run: |
echo "❌ 배포 실패"
exit 1
10 changes: 7 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ _ReSharper*/

# Docker
**/Dockerfile.*
docker-compose*
docker-compose.override.yml

# Keep template files but ignore runtime files
!docker-compose.prod.yml
!env.prod.example

# Logs
*.log
Expand Down Expand Up @@ -175,5 +179,5 @@ secrets.json
*.xlsx

# Demo files
web_chat_demo.html
time_travel_commit.exe
*.exe
*.ini
6 changes: 3 additions & 3 deletions ProjectVG.Api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ EXPOSE 7900
# 빌드 결과 복사
COPY --from=build /app/publish .

# 헬스체크 추가
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:7900/health || exit 1
# 헬스체크 추가 - wget 사용
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:7900/health || exit 1

ENTRYPOINT ["dotnet", "ProjectVG.Api.dll"]
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public async Task GetConversationHistoryAsync_WithExistingMessages_ShouldReturnI
var message3 = await _conversationService.AddMessageAsync(userId, characterId, ChatRole.User, "Third message", DateTime.UtcNow);

// Act
var history = await _conversationService.GetConversationHistoryAsync(userId, characterId, 10);
var history = await _conversationService.GetConversationHistoryAsync(userId, characterId, 1, 10);

// Assert
var historyList = history.ToList();
Expand All @@ -160,7 +160,7 @@ public async Task GetConversationHistoryAsync_WithCountLimit_ShouldRespectLimit(
}

// Act - Request only 3 messages
var history = await _conversationService.GetConversationHistoryAsync(userId, characterId, 3);
var history = await _conversationService.GetConversationHistoryAsync(userId, characterId, 1, 3);

// Assert
var historyList = history.ToList();
Expand Down Expand Up @@ -215,7 +215,7 @@ public async Task GetConversationHistoryAsync_WithInvalidCount_ShouldThrowValida

// Act & Assert
await Assert.ThrowsAsync<ValidationException>(
() => _conversationService.GetConversationHistoryAsync(userId, characterId, count));
() => _conversationService.GetConversationHistoryAsync(userId, characterId, 1, count));
}

[Fact]
Expand All @@ -225,8 +225,8 @@ public async Task GetConversationHistoryAsync_WithMultipleUserCharacterPairs_Sho
await _fixture.ClearDatabaseAsync();
var user1 = await CreateUserAsync("user1", "[email protected]");
var user2 = await CreateUserAsync("user2", "[email protected]");
var char1 = await CreateCharacterAsync("Character1");
var char2 = await CreateCharacterAsync("Character2");
var char1 = await CreateCharacterAsync("Character1", user1.Id);
var char2 = await CreateCharacterAsync("Character2", user1.Id);

// Add messages for different user-character combinations
await _conversationService.AddMessageAsync(user1.Id, char1.Id, ChatRole.User, "User1-Char1 Message", DateTime.UtcNow);
Expand Down Expand Up @@ -291,8 +291,8 @@ public async Task ClearConversationAsync_ShouldOnlyAffectSpecificUserCharacterPa
await _fixture.ClearDatabaseAsync();
var user1 = await CreateUserAsync("user1", "[email protected]");
var user2 = await CreateUserAsync("user2", "[email protected]");
var char1 = await CreateCharacterAsync("Character1");
var char2 = await CreateCharacterAsync("Character2");
var char1 = await CreateCharacterAsync("Character1", user1.Id);
var char2 = await CreateCharacterAsync("Character2", user1.Id);

// Add messages for different combinations
await _conversationService.AddMessageAsync(user1.Id, char1.Id, ChatRole.User, "User1-Char1", DateTime.UtcNow);
Expand Down Expand Up @@ -429,8 +429,8 @@ public async Task MultipleConversationsSimultaneously_ShouldIsolateCorrectly()
await _fixture.ClearDatabaseAsync();
var user1 = await CreateUserAsync("user1", "[email protected]");
var user2 = await CreateUserAsync("user2", "[email protected]");
var character1 = await CreateCharacterAsync("Character1");
var character2 = await CreateCharacterAsync("Character2");
var character1 = await CreateCharacterAsync("Character1", user1.Id);
var character2 = await CreateCharacterAsync("Character2", user1.Id);

// Create conversations for different user-character pairs
// User1 with Character1
Expand Down Expand Up @@ -473,7 +473,7 @@ public async Task MultipleConversationsSimultaneously_ShouldIsolateCorrectly()
private async Task<(Guid userId, Guid characterId)> CreateUserAndCharacterAsync()
{
var user = await CreateUserAsync();
var character = await CreateCharacterAsync();
var character = await CreateCharacterAsync("TestCharacter", user.Id);
return (user.Id, character.Id);
}

Expand All @@ -486,9 +486,9 @@ public async Task MultipleConversationsSimultaneously_ShouldIsolateCorrectly()
}

private async Task<ProjectVG.Application.Models.Character.CharacterDto> CreateCharacterAsync(
string name = "TestCharacter")
string name = "TestCharacter", Guid? userId = null)
{
var createCommand = TestDataBuilder.CreateCreateCharacterWithFieldsCommand(name);
var createCommand = TestDataBuilder.CreateCreateCharacterWithFieldsCommand(name, userId: userId);
return await _characterService.CreateCharacterWithFieldsAsync(createCommand);
}

Expand Down
Loading
Loading