Genesis_GameServer
유니티 게임(Genesis) 서버 + 개인 포트폴리오 사이트.
하나의 ASP.NET Core 프로세스가 두 가지를 함께 서빙합니다.
| 경로 | 내용 |
|---|---|
/myGame/* |
유니티 게임 클라이언트가 쓰는 기존 API (변경 없음) |
/api/portfolio/* |
포트폴리오 REST API |
/ 그 밖의 모든 경로 |
포트폴리오 웹사이트 (정적 SPA) |
1. 처음 실행하기
관리자 비밀번호부터 바꾸세요
GameServer/appsettings.json 의 Portfolio.AdminPassword 가 CHANGE_ME 로 되어 있으면
로그인 자체가 막혀 있습니다(503). 원하는 비밀번호로 바꾸고 서버를 다시 시작하세요.
"Portfolio": {
"AdminPassword": "여기에-원하는-비밀번호",
"SiteTitle": "사이트 이름",
"SiteTagline": "한 줄 소개",
"MaxUploadBytes": 10485760
}
SiteTitle·SiteTagline은 지금 어느 코드도 읽지 않습니다. 화면에 나오는 이름과 문구는GameServer/wwwroot/index.html에 직접 들어 있습니다.
파일에 비밀번호를 두고 싶지 않다면 환경변수가 우선합니다.
# Windows (PowerShell)
$env:Portfolio__AdminPassword = "비밀번호"
# Linux / macOS
export Portfolio__AdminPassword="비밀번호"
실행
dotnet run --project GameServer/GameServer.csproj
브라우저에서 http://localhost:5281 을 열면 포트폴리오가 뜹니다.
우측 상단 관리자 버튼 → 비밀번호 입력 → 글을 쓸 수 있습니다.
이 프로젝트는
net9.0을 대상으로 합니다. .NET 9 런타임이 없고 상위 버전만 있다면DOTNET_ROLL_FORWARD=LatestMajor를 주고 실행하거나, .NET 9 런타임을 설치하세요.
2. 데이터베이스
포트폴리오 테이블은 서버가 시작할 때 자동으로 만들어집니다
(Data/PortfolioSchemaInitializer.cs, CREATE TABLE IF NOT EXISTS).
따로 마이그레이션을 돌릴 필요가 없습니다.
tb_portfolio_category— 카테고리 (처음 한 번게임 / 개발 / 아트 / 기록4개가 시드됩니다)tb_portfolio_post— 게시물tb_portfolio_post_image— 게시물 이미지
수동으로 만들고 싶다면 GameServer/sql/portfolio_schema.sql 을 쓰세요.
DB 연결이 실패해도 게임 서버 부팅은 막지 않습니다(로그만 남기고 넘어갑니다).
3. 화면
| 주소 | 화면 |
|---|---|
#/ |
작업 인덱스 (히어로 + 카테고리 필터 + 번호 매긴 목록) |
#/?cat=game |
카테고리로 걸러진 목록 |
#/post/12 |
게시물 상세 (마크다운 본문 + 이미지 갤러리 + 라이트박스) |
#/about |
소개 |
#/admin |
관리자 로그인 / 대시보드 |
#/admin/new |
새 글 |
#/admin/edit/12 |
글 수정 |
#/admin/categories |
카테고리 관리 |
글쓰기
본문은 마크다운입니다. 왼쪽에 쓰면 오른쪽에 바로 미리보기가 나옵니다.
이미지는 세 가지 방법으로 올립니다.
- 이미지 영역을 클릭해서 파일 고르기
- 파일을 끌어다 놓기
- 클립보드에서 Ctrl+V 로 붙여넣기
올린 이미지마다 세 개의 버튼이 있습니다.
- 대표 — 목록에서 커서를 따라 뜨는 썸네일과 상세 표지로 지정
- 본문삽입 — 커서 위치에
를 끼워 넣기 - 삭제 — 목록에서 빼기 (이번에 올린 파일이면 서버에서도 지웁니다)
저장하지 않고 페이지를 벗어나려 하면 경고가 뜹니다.
화면 구성
목록은 카드 그리드가 아니라 작품집 목차(인덱스) 형태입니다. 번호 → 제목 → 요약 → 카테고리 · 날짜 순으로 한 줄씩 늘어놓고, 행에 마우스를 올리면 행이 살짝 밀려 들어오면서 대표 이미지가 커서 옆에 떠오릅니다 (마우스가 있는 기기에서만. 터치 기기에서는 뜨지 않습니다).
배경 장식은 쓰지 않습니다. 위계는 글자 크기 대비와 얇은 괘선으로 만들고, 색은 카테고리 강조색으로만 들어옵니다. 한글은 산세리프, 숫자·라벨은 모노스페이스입니다.
카테고리 색은 #/admin/categories 에서 바꿀 수 있고, 그 색이 목록·상세·관리자 화면에
그대로 반영됩니다.
샘플 글 지우기
화면이 어떻게 보이는지 확인할 수 있도록 제목이 [샘플] 로 시작하는 글 5건을
자리표시 이미지와 함께 넣어 두었습니다. 직접 쓴 글이 아니니 지우고 시작하세요.
#/admin 에서 로그인한 뒤 각 줄의 삭제 를 누르면 됩니다.
글을 지워도 업로드된 자리표시 이미지 파일은 남으므로, 함께 비우려면
GameServer/wwwroot/uploads/ 아래 폴더를 통째로 지우면 됩니다
(.gitkeep 은 남겨 두세요).
소개 화면 내용 바꾸기
GameServer/wwwroot/js/views/about.js 의 ABOUT_MD 상수 하나만 고치면 됩니다.
빌드가 필요 없고 새로고침하면 바로 반영됩니다.
4. 구조
GameServer/
├─ Controllers/
│ ├─ CharacterController.cs 게임 API (기존)
│ ├─ UserController.cs 게임 API (기존)
│ └─ Portfolio/
│ ├─ PortfolioAuthController.cs 로그인 · 로그아웃 · 상태
│ ├─ PortfolioCategoryController.cs 카테고리 CRUD
│ ├─ PortfolioPostController.cs 게시물 CRUD
│ └─ PortfolioUploadController.cs 이미지 업로드 · 삭제
├─ Models/Portfolio/ 모델 + DTO
├─ Services/PortfolioAdminAuth.cs 관리자 비밀번호 검증
├─ Data/PortfolioSchemaInitializer.cs 테이블 자동 생성 + 시드
├─ sql/portfolio_schema.sql 참고용 DDL
└─ wwwroot/ 포트폴리오 사이트 (빌드 단계 없음)
├─ index.html
├─ css/style.css 디자인 시스템 한 파일
├─ js/
│ ├─ app.js 진입점 · 라우트 표
│ ├─ router.js 해시 라우터
│ ├─ api.js REST 래퍼
│ ├─ ui.js 토스트 · 모달 · 헬퍼
│ ├─ markdown.js 자체 제작 마크다운 렌더러
│ └─ views/ home · post · about · admin
└─ uploads/ 업로드된 이미지 (git 에서 제외됨)
프론트엔드에 빌드 단계가 없습니다. npm 도, 번들러도, 외부 CDN 요청도 없습니다.
배포는 그대로 dotnet publish 하나로 끝납니다.
5. 리눅스에 배포하기 — 도커 없이
런타임까지 통째로 묶은 자체 포함(self-contained) 실행 파일을 만들어 올리는 방식입니다. 서버에 .NET 을 설치할 필요가 없습니다. Visual Studio 게시 대화상자의 "배포 모드: 자체 포함 / 대상 런타임: linux-x64" 와 같은 결과물입니다.
만들기
윈도우에서 만들어도 됩니다. 리눅스용 실행 파일이 나옵니다.
PowerShell (VS Code 기본 터미널)
.\deploy\publish-linux.cmd
Git Bash · WSL · 맥 · 리눅스
./deploy/publish-linux.sh
셋 다 같은 일을 합니다. 상황에 맞는 것을 쓰세요.
| 파일 | 쓰는 곳 |
|---|---|
publish-linux.cmd |
PowerShell·cmd. 실행 정책을 건드리지 않아도 됩니다 |
publish-linux.ps1 |
실제 내용. 아래 정책 문제를 해결했다면 직접 실행해도 됩니다 |
publish-linux.sh |
Git Bash · WSL · 맥 · 리눅스 |
.ps1 이 "이 시스템에서 스크립트를 실행할 수 없으므로" 오류가 날 때
윈도우 기본 실행 정책이 Restricted 라 .ps1 이 막힙니다. 세 가지 방법이 있습니다.
-
.cmd를 쓴다 — 가장 간단합니다. 정책을 바꾸지 않습니다..\deploy\publish-linux.cmdcmd파일은 앞에.\를 꼭 붙이세요. 없으면 현재 폴더를 찾지 못합니다. -
한 번만 우회한다 — 시스템 설정을 바꾸지 않습니다.
powershell -ExecutionPolicy Bypass -File .\deploy\publish-linux.ps1 -
내 계정에만 정책을 푼다 — 앞으로 계속
.ps1을 쓰고 싶다면.Set-ExecutionPolicy -Scope CurrentUser RemoteSignedRemoteSigned는 직접 만든 스크립트는 실행하고, 인터넷에서 받은 것은 서명을 요구합니다. 관리자 권한이 필요 없고 이 계정에만 적용됩니다.
artifacts/linux-x64/ 에 이런 결과가 나옵니다. 53MB, 파일 네 개뿐입니다.
GameServer 리눅스 실행 파일 (런타임 포함, 단일 파일)
GameServer.staticwebassets.endpoints.json
appsettings.json
wwwroot/ 사이트 (html · css · js)
스크립트가 쓰는 명령은 이것 하나입니다.
dotnet publish GameServer/GameServer.csproj -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true -o artifacts/linux-x64
PublishTrimmed는 켜지 마세요. 용량은 더 줄지만 EF Core 가 리플렉션으로 찾는 타입이 잘려 나가 실행 중에 터집니다.
서버에서 바로 실행해 보기
복사한 폴더로 들어가 run.sh 를 실행하면 끝입니다. 서버에 .NET 을 깔 필요가 없습니다.
cd /srv/genesis
chmod +x run.sh
./run.sh # PORT=8080 ./run.sh 처럼 포트를 바꿀 수 있습니다
run.sh 가 두 가지를 대신 해 줍니다.
- 실행 권한 — 윈도우에서 복사하면 실행 비트가 사라져
Permission denied가 납니다. - 바인딩 주소 —
ASPNETCORE_URLS를 주지 않으면localhost:5000에만 묶여 서버 안에서만 보이고 브라우저로는 안 열립니다.0.0.0.0:5281로 열어 줍니다.
클라우드 서버라면 방화벽도 열어야 합니다. sudo ufw allow 5281/tcp
SSH 를 끊으면 함께 종료되므로, 계속 띄워 두려면 아래 systemd 등록을 하세요.
이미 돌고 있는 서버 멈추기
새로 올린 것이 같은 포트를 쓰려면 기존 프로세스를 먼저 내려야 합니다.
Address already in use 가 뜨면 이 경우입니다.
1) 무엇이 포트를 잡고 있는지 본다
sudo ss -tlnp | grep 5281 # 없으면: sudo lsof -i :5281
ps aux | grep -iE 'GameServer|dotnet' | grep -v grep
systemctl list-units --type=service | grep -iE 'genesis|game|portfolio'
docker ps # 도커로 띄웠다면
2) 나온 결과에 맞춰 끈다
| 어떻게 떠 있나 | 끄는 법 |
|---|---|
| systemd 서비스 | sudo systemctl stop 이름 |
| 도커 컨테이너 | docker stop 이름 |
| 그냥 실행한 프로세스 | kill -INT <PID> |
| screen · tmux 안 | screen -r / tmux attach 후 Ctrl+C |
systemd 로 등록돼 있다면 kill 은 소용없습니다. Restart=always 때문에
바로 되살아납니다. 반드시 systemctl stop 을 쓰고, 다시 안 뜨게 하려면
sudo systemctl disable 이름 까지 하세요.
3) 정말 내려갔는지 확인
sudo ss -tlnp | grep 5281 # 아무것도 안 나오면 성공
.NET 은
SIGINT(kill -INT) 를 받아야 정상 종료합니다. 그냥kill(SIGTERM) 이나kill -9는 쓰던 파일을 정리하지 못하고 끊길 수 있습니다.
서버 준비 (처음 한 번)
sudo useradd -r -s /usr/sbin/nologin genesis
sudo mkdir -p /srv/genesis/wwwroot/uploads
sudo chown -R genesis:genesis /srv/genesis
# 비밀값
sudo cp deploy/genesis.env.example /srv/genesis/genesis.env
sudo nano /srv/genesis/genesis.env
sudo chown genesis:genesis /srv/genesis/genesis.env
sudo chmod 600 /srv/genesis/genesis.env
# 서비스 등록
sudo cp deploy/genesis.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now genesis
올리기 · 갱신
.\deploy\publish-linux.ps1 -Target user@서버주소 # PowerShell
./deploy/publish-linux.sh user@서버주소 # bash
빌드 → 전송 → 실행 권한 부여까지 한 번에 합니다.
(bash 판은 rsync 를, PowerShell 판은 scp 를 씁니다. 둘 다 서버의 사진을 지우지 않습니다.)
업로드된 사진(wwwroot/uploads)과 genesis.env 는 건드리지 않습니다.
수동으로 할 때도 이 두 가지는 반드시 지켜야 합니다.
rsync -avz --delete --exclude 'wwwroot/uploads/' --exclude 'genesis.env' artifacts/linux-x64/ user@서버:/srv/genesis/
ssh user@서버 "chmod +x /srv/genesis/GameServer && sudo systemctl restart genesis"
chmod +x를 빼먹기 쉽습니다. 윈도우에서 복사하면 실행 권한이 사라져Permission denied로 서비스가 안 뜹니다.
배포 후 확인
산출물에 들어 있는 healthcheck.sh 를 서버에서 실행하면 한 번에 점검합니다.
cd /home/ubuntu/gameserver # 앱이 있는 폴더
./healthcheck.sh 5000 GameServer # 포트, 서비스 이름
이런 것들을 확인합니다.
- 서비스가
active (running)인지, 반복 재시작 중은 아닌지 - 포트를 실제로 듣고 있는지
- 기존 게임 API (
/myGame/*) 가 그대로 되는지 - 포트폴리오 API 와 카테고리 개수 (0 개면 DB 스키마 생성 실패)
- 사이트 정적 파일 (
/,css,js) - 업로드 폴더 쓰기 권한
- 최근 로그의 오류
실패한 항목이 있으면 종료 코드 1 을 돌려주므로 자동화에도 쓸 수 있습니다.
직접 보고 싶다면:
sudo systemctl status GameServer
sudo journalctl -u GameServer -n 100 --no-pager
sudo journalctl -u GameServer -f # 실시간
sudo ss -tlnp | grep 5000
curl -s localhost:5000/api/portfolio/categories
정상이라면 로그에 이 세 줄이 보입니다.
포트폴리오 스키마 준비 완료 (카테고리 N건).
Now listening on: http://0.0.0.0:5000
Application started. Press Ctrl+C to shut down.
Failed to determine the https port for redirect.경고는 무시해도 됩니다. HTTP 로만 띄웠을 때 늘 나오는 안내입니다.
어느 방식을 고를까
| 자체 포함 배포 | 도커 | |
|---|---|---|
| 서버에 필요한 것 | 없음 | Docker |
| 전송량 | 53MB | 이미지 레이어 |
| 격리 | systemd 수준 | 컨테이너 |
| 롤백 | 이전 폴더로 교체 | 이전 이미지 태그 |
혼자 쓰는 서버라면 자체 포함 배포가 더 단순합니다. 도커 쪽은 아래 6절에 있습니다.
6. 리눅스 컨테이너로 배포하기
준비물
로컬(윈도우)에서 이미지를 만들 필요는 없습니다. 서버에서 바로 빌드하는 쪽이 간단합니다.
-
서버: Docker Engine + Compose 플러그인
curl -fsSL https://get.docker.com | sudo sh sudo usermod -aG docker $USER # 다시 로그인해야 적용됩니다 -
VS Code(선택): 확장 두 개
- Docker (
ms-azuretools.vscode-docker) — Dockerfile 문법 지원, 이미지·컨테이너 목록, 우클릭 빌드/실행 - Remote - SSH (
ms-vscode-remote.remote-ssh) — 서버에 붙어서 그대로 편집·실행
Visual Studio 의 "Docker 지원 추가 → 게시" 같은 원클릭 배포는 VS Code 에 없습니다. 대신 Remote-SSH 로 서버에 붙어 아래 명령을 터미널에서 실행하는 흐름이 표준입니다.
- Docker (
첫 배포
git clone <저장소 주소> genesis && cd genesis
cp .env.example .env
nano .env # DB 연결 문자열 · 관리자 비밀번호 입력
mkdir -p uploads
sudo chown -R 1654:1654 uploads # 컨테이너의 app 사용자가 쓸 수 있게
docker compose up -d --build
http://서버주소:5281 로 접속됩니다.
다시 배포
git pull
docker compose up -d --build
업로드된 사진은 호스트의 ./uploads 에 있으므로 재배포해도 그대로 남습니다.
확인 · 문제 해결
docker compose ps # 상태 (healthy 여야 정상)
docker compose logs -f # 로그
docker compose down # 중지
- 업로드가 403/500 으로 실패 →
uploads폴더 소유자 문제입니다.sudo chown -R 1654:1654 uploads를 다시 확인하세요. - DB 에 못 붙음 → 컨테이너 안에서
localhost는 컨테이너 자신입니다. DB 가 같은 서버에 있어도.env에는 실제 IP 를 적어야 합니다. - 앞에 nginx 를 두는 경우 →
docker-compose.yml의 포트를"127.0.0.1:5281:8080"으로 바꿔 외부 직접 접근을 막으세요.
컨테이너 없이 배포할 때
dotnet publish 산출물만 올려도 됩니다. 이때 업로드 폴더는 덮어쓰지 마세요.
GameServer.csproj 에서 wwwroot/uploads 를 배포 산출물에서 빼 두었으므로
publish 결과에는 사진이 들어가지 않습니다(12MB). 서버의 기존 wwwroot/uploads 는
그대로 두고 나머지 파일만 교체하면 됩니다.
7. 보안 메모
- 조회는 누구나, 작성 · 수정 · 삭제는 로그인한 관리자만 가능합니다 (쿠키 세션).
- 로그인은 IP별 5분에 8회로 제한됩니다.
- 업로드는 확장자뿐 아니라 매직 바이트까지 검사합니다. 파일명은 서버가 GUID 로 새로 짓습니다.
- 이미지 삭제는 경로를 절대경로로 해석해
uploads폴더 밖이면 거부합니다. - 마크다운은 먼저 escape 한 뒤 HTML 을 조립하므로 본문에 넣은 태그가 실행되지 않습니다.
링크는
http·https·mailto와 사이트 내부 경로만 허용합니다.
아직 남은 것
appsettings.json 에 DB 비밀번호가 평문으로 들어 있고 git 에 커밋돼 있습니다.
이 저장소를 공개할 계획이 있다면 연결 문자열을 환경변수
(ConnectionStrings__DefaultConnection)나 사용자 비밀로 옮기고,
노출된 DB 비밀번호를 교체하는 것을 권합니다.