2026-09-04 01:44:59 +09:00
2026-09-03 19:18:46 +09:00
2026-09-04 01:44:59 +09:00
2026-09-04 01:44:59 +09:00
2026-09-04 01:44:59 +09:00
2026-09-04 01:44:59 +09:00
2026-09-04 01:44:59 +09:00
2026-09-04 01:44:59 +09:00
2026-09-04 01:44:59 +09:00
2026-09-04 01:44:59 +09:00

Genesis_GameServer

유니티 게임(Genesis) 서버 + 개인 포트폴리오 사이트.

하나의 ASP.NET Core 프로세스가 두 가지를 함께 서빙합니다.

경로 내용
/myGame/* 유니티 게임 클라이언트가 쓰는 기존 API (변경 없음)
/api/portfolio/* 포트폴리오 REST API
/ 그 밖의 모든 경로 포트폴리오 웹사이트 (정적 SPA)

1. 처음 실행하기

관리자 비밀번호부터 바꾸세요

GameServer/appsettings.jsonPortfolio.AdminPasswordCHANGE_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 카테고리 관리

글쓰기

본문은 마크다운입니다. 왼쪽에 쓰면 오른쪽에 바로 미리보기가 나옵니다.

이미지는 세 가지 방법으로 올립니다.

  1. 이미지 영역을 클릭해서 파일 고르기
  2. 파일을 끌어다 놓기
  3. 클립보드에서 Ctrl+V 로 붙여넣기

올린 이미지마다 세 개의 버튼이 있습니다.

  • 대표 — 목록에서 커서를 따라 뜨는 썸네일과 상세 표지로 지정
  • 본문삽입 — 커서 위치에 ![](주소) 를 끼워 넣기
  • 삭제 — 목록에서 빼기 (이번에 올린 파일이면 서버에서도 지웁니다)

저장하지 않고 페이지를 벗어나려 하면 경고가 뜹니다.

화면 구성

목록은 카드 그리드가 아니라 작품집 목차(인덱스) 형태입니다. 번호 → 제목 → 요약 → 카테고리 · 날짜 순으로 한 줄씩 늘어놓고, 행에 마우스를 올리면 행이 살짝 밀려 들어오면서 대표 이미지가 커서 옆에 떠오릅니다 (마우스가 있는 기기에서만. 터치 기기에서는 뜨지 않습니다).

배경 장식은 쓰지 않습니다. 위계는 글자 크기 대비와 얇은 괘선으로 만들고, 색은 카테고리 강조색으로만 들어옵니다. 한글은 산세리프, 숫자·라벨은 모노스페이스입니다.

카테고리 색은 #/admin/categories 에서 바꿀 수 있고, 그 색이 목록·상세·관리자 화면에 그대로 반영됩니다.

샘플 글 지우기

화면이 어떻게 보이는지 확인할 수 있도록 제목이 [샘플] 로 시작하는 글 5건을 자리표시 이미지와 함께 넣어 두었습니다. 직접 쓴 글이 아니니 지우고 시작하세요.

#/admin 에서 로그인한 뒤 각 줄의 삭제 를 누르면 됩니다. 글을 지워도 업로드된 자리표시 이미지 파일은 남으므로, 함께 비우려면 GameServer/wwwroot/uploads/ 아래 폴더를 통째로 지우면 됩니다 (.gitkeep 은 남겨 두세요).

소개 화면 내용 바꾸기

GameServer/wwwroot/js/views/about.jsABOUT_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 이 막힙니다. 세 가지 방법이 있습니다.

  1. .cmd 를 쓴다 — 가장 간단합니다. 정책을 바꾸지 않습니다.

    .\deploy\publish-linux.cmd
    

    cmd 파일은 앞에 .\ 를 꼭 붙이세요. 없으면 현재 폴더를 찾지 못합니다.

  2. 한 번만 우회한다 — 시스템 설정을 바꾸지 않습니다.

    powershell -ExecutionPolicy Bypass -File .\deploy\publish-linux.ps1
    
  3. 내 계정에만 정책을 푼다 — 앞으로 계속 .ps1 을 쓰고 싶다면.

    Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
    

    RemoteSigned 는 직접 만든 스크립트는 실행하고, 인터넷에서 받은 것은 서명을 요구합니다. 관리자 권한이 필요 없고 이 계정에만 적용됩니다.

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 attachCtrl+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    # 포트, 서비스 이름

이런 것들을 확인합니다.

  1. 서비스가 active (running) 인지, 반복 재시작 중은 아닌지
  2. 포트를 실제로 듣고 있는지
  3. 기존 게임 API (/myGame/*) 가 그대로 되는지
  4. 포트폴리오 API 와 카테고리 개수 (0 개면 DB 스키마 생성 실패)
  5. 사이트 정적 파일 (/, css, js)
  6. 업로드 폴더 쓰기 권한
  7. 최근 로그의 오류

실패한 항목이 있으면 종료 코드 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 로 서버에 붙어 아래 명령을 터미널에서 실행하는 흐름이 표준입니다.

첫 배포

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.jsonDB 비밀번호가 평문으로 들어 있고 git 에 커밋돼 있습니다. 이 저장소를 공개할 계획이 있다면 연결 문자열을 환경변수 (ConnectionStrings__DefaultConnection)나 사용자 비밀로 옮기고, 노출된 DB 비밀번호를 교체하는 것을 권합니다.

Description
유니티 게임 서버
Readme 2.3 MiB
Languages
CSS 38.6%
JavaScript 32.7%
C# 23.4%
Shell 2.4%
PowerShell 1.4%
Other 1.4%