오랜만에 커밋

This commit is contained in:
2026-05-04 10:30:04 +09:00
parent c35c042afa
commit 722d5dbfaf
56 changed files with 2288 additions and 11 deletions
+22
View File
@@ -0,0 +1,22 @@
---
id: "Aider 단축키 20260413"
created: "2026-04-13 13:35"
tags:
---
| **명령어** | **설명** | **비고** |
| --------------- | --------------------------------- | ----------------------------------- |
| **/undo** | **AI가 방금 수정한 코드를 취소하고 직전 상태로 복구** | 가장 자주 쓰임 |
| **/add <파일>** | 수정하거나 참고할 파일을 채팅창에 추가 | 파일명 일부만 쳐도 자동완성 |
| **/drop <파일>** | 채팅창에서 특정 파일 제외 | 실제 파일이 삭제되지는 않음 |
| **/ls** | 현재 채팅창에 포함된 파일 목록 확인 | |
| **/diff** | 마지막 수정 사항의 변경 전/후 차이점 표시 | |
| **/test <명령어>** | 테스트 명령을 실행하고 실패 시 AI가 자동 수정 | 예: `/test pytest` |
| **/model <모델>** | 사용 중인 AI 모델을 즉시 변경 | 예: `/model gemini/gemini-2.5-flash` |
| **/tokens** | 현재 세션의 토큰 사용량과 비용 정보 확인 | |
| **/run <명령어>** | 에이더를 나가지 않고 터미널 명령어 실행 | 예: `/run ls -al` |
| **/exit** | 에이더 종료 | `/quit`과 동일 |
| **/help** | 모든 명령어 목록과 상세 도움말 확인 | |
| **명령어** | **용도** | **특징** |
| **/ask <질문>** | 코드에 대해 질문하기 | 코드를 **직접 수정하지 않고** 설명만 해줍니다. |
| **/chat <메시지>** | 일반적인 대화/토론 | 파일 수정 없이 구현 방법이나 구조에 대해 논의할 때 씁니다. |
| **/code <명령>** | 코드 수정 요청 | 기본 모드입니다. 지시한 대로 파일을 직접 수정합니다. |
+689
View File
@@ -0,0 +1,689 @@
---
id: "Aider 최초 사용 20260413"
created: "2026-04-13 14:01"
tags:
---
Gemini를 채찍질해서 아래의 명세를 가지는 VaultKeeper 어플리케이션 구상을 함.
( kui-veil 서버의 auth 관리 어플리케이션임 )
```markdown
## 🏗️ VaultKeeper 프로젝트 상세 설계도 (Ver 2.0)
### 1. 개요
- **목적**: Nginx의 `.htpasswd` 파일을 웹 GUI를 통해 관리하는 초경량 도구.
- **타겟 환경**: Google Cloud e2-micro (저사양 서버).
- **주요 언어**: **Go** (백엔드), **Vanilla JS** (프론트엔드).
### 2. 보안 전략 (핵심)
- **인증 분리**: 백엔드 자체에 로그인 로직을 구현하지 않고, Nginx의 `auth_basic` 모듈에 인증을 위임함.
- **접근 권한**: 서버 관리자가 터미널에서 직접 생성한 계정으로만 웹 UI에 접근 가능.
- **입력값 검증**: `os/exec` 실행 시 쉘 인젝션을 방지하기 위해 사용자 이름에 특수문자 입력을 제한함.
### 3. 구성 요소
- **Backend (`main.go`)**:
- 환경 변수 `HTPASSWD_PATH`를 통해 대상 파일 경로 지정.
- 표준 패키지 `net/http`를 사용하여 메모리 점유 최소화.
- `htpasswd` 시스템 명령어를 서브프로세스로 호출하여 계정 추가/삭제.
- **Frontend (`index.html`)**:
- 단일 파일 SPA 구조.
- Tailwind CSS(CDN)를 활용한 직관적인 대시보드.
```
```markdown
## 🤖 에이더(Aider)를 위한 최종 지시서
아래 내용을 에이더 채팅창에 입력하시면 됩니다.
**프로젝트: VaultKeeper (Nginx .htpasswd 관리 도구)**
**1. 백엔드 구현 (`main.go`)**
- **언어**: Go (표준 라이브러리 위주로 매우 가볍게).
- **기능**:
- `GET /api/users`: `.htpasswd` 파일을 읽어 유저 목록을 JSON으로 반환.
- `POST /api/users`: `username`, `password`를 받아 `htpasswd -b` 실행.
- `DELETE /api/users/{username}`: `htpasswd -D` 실행.
- **보안**: 사용자 이름 입력값에서 쉘 인젝션이 발생하지 않도록 유효성 검사 로직 포함.
- **설정**: `.htpasswd` 경로를 환경 변수 `HTPASSWD_PATH`에서 읽어오도록 설정.
**2. 프론트엔드 구현 (`index.html`)**
- 단일 파일 SPA로 작성하고 Tailwind CSS를 사용하여 깔끔한 UI 제공.
- 유저 목록 표시, 추가 폼, 삭제 버튼 구현.
**3. 배포 가이드 (`deployment.md`) 생성**
- `go build`를 이용한 경량 바이너리 빌드 방법 (`-s -w` 옵션 포함).
- 터미널에서 최초 관리자 계정을 생성하는 명령어 안내.
- 이 서비스를 Nginx `auth_basic` 뒤에 배치하기 위한 `location` 블록 설정 예시. (8000번 포트 기준)
**주의**: 모든 코드와 문서에서 특정 개인의 이름이나 개인정보를 절대 포함하지 말고 `Admin` 또는 `User`와 같은 공용 용어만 사용해줘.
```
이걸 aider에게 먹이고 (add)
```bash
────────────────────────────────────────────────────────────────────────────────
> /add "doc/vault-keeper 상세 설계도.txt" "doc/지시서.txt"
Added doc\vault-keeper 상세 설계도.txt to the chat
Added doc\지시서.txt to the chat
────────────────────────────────────────────────────────────────────────────────
```
진행해보라고 했다.
```bash
doc\vault-keeper 상세 설계도.txt doc\지시서.txt
> 내가 추가한 상세 설계도와 지시서를 바탕으로 어플리케이션을 만들어봐
```
과연.. 결과는?
어.. 뭔가 만들어지긴 했다..
아래는 콘솔에 찍힌 내용 전체
요청하신 내용에 따라 애플리케이션을 만들겠습니다. main.go, index.html, deployment.md 파일을 새로 생성합니다.
다음은 필요한 SEARCH/REPLACE 블록입니다:
main.go
<<<<<<< SEARCH
=======
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"regexp"
"strings"
"sync"
)
// User는 .htpasswd 파일의 사용자 정보를 나타냅니다.
type User struct {
Username string `json:"username"`
}
// AddUserRequest는 새 사용자 추가 요청에 사용됩니다.
type AddUserRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
var (
htpasswdPath string
htpasswdMutex sync.Mutex // htpasswd 명령 실행 시 동시성 문제를 방지하기 위한 뮤텍스
usernameRegex *regexp.Regexp // 사용자 이름 유효성 검사를 위한 정규식
)
func init() {
// HTPASSWD_PATH 환경 변수 설정
htpasswdPath = os.Getenv("HTPASSWD_PATH")
if htpasswdPath == "" {
log.Fatal("환경 변수 HTPASSWD_PATH가 설정되지 않았습니다.")
}
// 사용자 이름 유효성 검사 정규식 초기화: 영문, 숫자, 하이픈, 언더스코어만 허용
usernameRegex = regexp.MustCompile("^[a-zA-Z0-9_-]+$")
}
func main() {
mux := http.NewServeMux()
// 정적 파일(index.html) 제공
mux.HandleFunc("/", serveFrontend)
// API 엔드포인트
mux.HandleFunc("/api/users", handleUsers)
mux.HandleFunc("/api/users/", handleDeleteUser) // DELETE 요청 처리
log.Println("VaultKeeper 서버가 8000 포트에서 시작됩니다.")
log.Printf("HTPASSWD_PATH: %s\n", htpasswdPath)
log.Fatal(http.ListenAndServe(":8000", mux))
}
// serveFrontend는 index.html 파일을 제공합니다.
func serveFrontend(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
http.ServeFile(w, r, "index.html")
return
}
http.NotFound(w, r)
}
// handleUsers는 GET /api/users 및 POST /api/users 요청을 처리합니다.
func handleUsers(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getUsers(w, r)
case http.MethodPost:
addUser(w, r)
default:
http.Error(w, "허용되지 않는 메소드입니다.", http.StatusMethodNotAllowed)
}
}
// getUsers는 .htpasswd 파일에서 사용자 목록을 읽어 JSON으로 반환합니다.
func getUsers(w http.ResponseWriter, r *http.Request) {
htpasswdMutex.Lock()
defer htpasswdMutex.Unlock()
file, err := os.Open(htpasswdPath)
if os.IsNotExist(err) {
// 파일이 없으면 빈 목록 반환 (처음 생성 시)
json.NewEncoder(w).Encode([]User{})
return
}
if err != nil {
log.Printf("Error opening .htpasswd file: %v", err)
http.Error(w, "사용자 파일을 읽을 수 없습니다.", http.StatusInternalServerError)
return
}
defer file.Close()
var users []User
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if len(line) > 0 && !strings.HasPrefix(line, "#") { // 주석 및 빈 줄 무시
parts := strings.SplitN(line, ":", 2)
if len(parts) > 0 {
users = append(users, User{Username: parts[0]})
}
}
}
if err := scanner.Err(); err != nil {
log.Printf("Error scanning .htpasswd file: %v", err)
http.Error(w, "사용자 파일을 스캔할 수 없습니다.", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
// addUser는 새 사용자를 .htpasswd 파일에 추가합니다.
func addUser(w http.ResponseWriter, r *http.Request) {
var req AddUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "유효하지 않은 요청 본문입니다.", http.StatusBadRequest)
return
}
// 사용자 이름 유효성 검사
if !usernameRegex.MatchString(req.Username) {
http.Error(w, "사용자 이름은 영문, 숫자, 하이픈, 언더스코어만 포함할 수 있습니다.", http.StatusBadRequest)
return
}
if req.Username == "" || req.Password == "" {
http.Error(w, "사용자 이름과 비밀번호는 필수입니다.", http.StatusBadRequest)
return
}
htpasswdMutex.Lock()
defer htpasswdMutex.Unlock()
// htpasswd -b <file> <username> <password>
// 파일이 없으면 -c 옵션이 필요하지만, 첫 생성은 터미널에서 하도록 가이드하므로 여기서는 -b만 사용.
// -b는 파일이 없으면 생성하고, 있으면 추가/수정합니다.
cmd := exec.Command("htpasswd", "-b", htpasswdPath, req.Username, req.Password)
output, err := cmd.CombinedOutput()
if err != nil {
log.Printf("Error executing htpasswd -b: %v, Output: %s", err, output)
http.Error(w, fmt.Sprintf("사용자 추가 실패: %s", strings.TrimSpace(string(output))), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"message": "사용자가 성공적으로 추가되었습니다."})
}
// handleDeleteUser는 DELETE /api/users/{username} 요청을 처리합니다.
func handleDeleteUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, "허용되지 않는 메소드입니다.", http.StatusMethodNotAllowed)
return
}
username := strings.TrimPrefix(r.URL.Path, "/api/users/")
if username == "" {
http.Error(w, "사용자 이름을 지정해야 합니다.", http.StatusBadRequest)
return
}
// 사용자 이름 유효성 검사 (삭제도 동일하게 적용)
if !usernameRegex.MatchString(username) {
http.Error(w, "유효하지 않은 사용자 이름 형식입니다.", http.StatusBadRequest)
return
}
htpasswdMutex.Lock()
defer htpasswdMutex.Unlock()
// htpasswd -D <file> <username>
cmd := exec.Command("htpasswd", "-D", htpasswdPath, username)
output, err := cmd.CombinedOutput()
if err != nil {
// htpasswd -D는 사용자가 존재하지 않을 경우에도 에러를 반환합니다.
// 그러나 사용자 삭제 요청 자체는 성공했다고 간주할 수 있습니다 (해당 사용자가 없는 상태이므로).
// 실제 htpasswd의 에러 메시지를 확인하여 사용자 부재인지 다른 에러인지 구분할 수 있으나,
// 간단하게 사용자 부재 메시지가 아니라면 서버 에러로 처리합니다.
if strings.Contains(strings.ToLower(string(output)), "not found") {
http.Error(w, fmt.Sprintf("사용자 '%s'를 찾을 수 없습니다.", username), http.StatusNotFound)
return
}
log.Printf("Error executing htpasswd -D: %v, Output: %s", err, output)
http.Error(w, fmt.Sprintf("사용자 삭제 실패: %s", strings.TrimSpace(string(output))), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(map[string]string{"message": fmt.Sprintf("사용자 '%s'가 성공적으로 삭제되었습니다.", username)})
}
>>>>>>> REPLACE
index.html
<<<<<<< SEARCH
=======
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VaultKeeper - Nginx .htpasswd 관리</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<style>
body {
background-color: #f3f4f6;
}
</style>
</head>
<body class="font-sans antialiased text-gray-900">
<div class="min-h-screen flex flex-col items-center justify-center p-4">
<div class="w-full max-w-2xl bg-white rounded-lg shadow-xl p-8 space-y-8">
<h1 class="text-3xl font-bold text-center text-gray-800">VaultKeeper</h1>
<p class="text-center text-gray-600">Nginx .htpasswd 파일 관리</p>
<!-- 사용자 추가 폼 -->
<div class="border-t border-gray-200 pt-8">
<h2 class="text-2xl font-semibold text-gray-800 mb-4">새 사용자 추가</h2>
<form id="addUserForm" class="space-y-4">
<div>
<label for="username" class="block text-sm font-medium text-gray-700">사용자 이름</label>
<input type="text" id="username" name="username" required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none
focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">비밀번호</label>
<input type="password" id="password" name="password" required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none
focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
</div>
<button type="submit"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm
font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
사용자 추가
</button>
</form>
<div id="addMessage" class="mt-3 text-center font-medium"></div>
</div>
<!-- 사용자 목록 -->
<div class="border-t border-gray-200 pt-8">
<h2 class="text-2xl font-semibold text-gray-800 mb-4">현재 사용자 목록</h2>
<div id="userListContainer">
<ul id="userList" class="divide-y divide-gray-200 bg-white rounded-md shadow-sm">
<!-- 사용자는 여기에 동적으로 로드됩니다. -->
</ul>
<p id="noUsersMessage" class="text-center text-gray-500 mt-4 hidden">등록된 사용자가 없습니다.</p>
</div>
<div id="listMessage" class="mt-3 text-center font-medium"></div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const addUserForm = document.getElementById('addUserForm');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const addMessageDiv = document.getElementById('addMessage');
const userList = document.getElementById('userList');
const noUsersMessage = document.getElementById('noUsersMessage');
const listMessageDiv = document.getElementById('listMessage');
async function fetchUsers() {
listMessageDiv.textContent = '';
userList.innerHTML = '';
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const users = await response.json();
if (users.length === 0) {
noUsersMessage.classList.remove('hidden');
} else {
noUsersMessage.classList.add('hidden');
users.forEach(user => {
const li = document.createElement('li');
li.className = 'flex items-center justify-between p-4 hover:bg-gray-50';
li.innerHTML = `
<span class="text-lg font-medium">${user.username}</span>
<button data-username="${user.username}"
class="delete-btn bg-red-500 hover:bg-red-600 text-white font-bold py-1 px-3 rounded text-sm">
삭제
</button>
`;
userList.appendChild(li);
});
addDeleteEventListeners();
}
} catch (error) {
console.error('Error fetching users:', error);
listMessageDiv.textContent = `사용자 목록을 불러오는 데 실패했습니다: ${error.message}`;
listMessageDiv.className = 'mt-3 text-center font-medium text-red-600';
}
}
async function addUser(event) {
event.preventDefault();
addMessageDiv.textContent = '';
const username = usernameInput.value;
const password = passwordInput.value;
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || '사용자 추가 실패');
}
addMessageDiv.textContent = data.message;
addMessageDiv.className = 'mt-3 text-center font-medium text-green-600';
usernameInput.value = '';
passwordInput.value = '';
fetchUsers(); // 사용자 목록 새로 고침
} catch (error) {
console.error('Error adding user:', error);
addMessageDiv.textContent = `사용자 추가 실패: ${error.message}`;
addMessageDiv.className = 'mt-3 text-center font-medium text-red-600';
}
}
async function deleteUser(username) {
listMessageDiv.textContent = '';
if (!confirm(`사용자 '${username}'을(를) 정말 삭제하시겠습니까?`)) {
return;
}
try {
const response = await fetch(`/api/users/${username}`, {
method: 'DELETE',
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || '사용자 삭제 실패');
}
listMessageDiv.textContent = data.message;
listMessageDiv.className = 'mt-3 text-center font-medium text-green-600';
fetchUsers(); // 사용자 목록 새로 고침
} catch (error) {
console.error('Error deleting user:', error);
listMessageDiv.textContent = `사용자 삭제 실패: ${error.message}`;
listMessageDiv.className = 'mt-3 text-center font-medium text-red-600';
}
}
function addDeleteEventListeners() {
document.querySelectorAll('.delete-btn').forEach(button => {
button.onclick = () => deleteUser(button.dataset.username);
});
}
addUserForm.addEventListener('submit', addUser);
// 페이지 로드 시 사용자 목록 가져오기
fetchUsers();
});
</script>
</body>
</html>
>>>>>>> REPLACE
deployment.md
<<<<<<< SEARCH
=======
# VaultKeeper 배포 가이드
이 문서는 VaultKeeper 애플리케이션의 빌드, 초기 설정 및 Nginx 연동 방법을 안내합니다.
## 1. 백엔드 빌드
Go 바이너리를 가볍게 빌드하려면 다음 명령어를 사용합니다. 이 명령어는 디버그 정보를 제거하고 심볼 테이블을 생략하여 최종 바이너리
크기를 줄입니다.
```bash
go build -o vaultkeeper -ldflags "-s -w" main.go
빌드가 완료되면 현재 디렉토리에 vaultkeeper 실행 파일이 생성됩니다.
2. .htpasswd 파일 경로 설정
VaultKeeper는 .htpasswd 파일의 경로를 환경 변수 HTPASSWD_PATH를 통해 설정합니다. 애플리케이션을 실행하기 전에 이 환경 변수를 설정해야
합니다.
예시:
export HTPASSWD_PATH="/etc/nginx/.htpasswd"
또는
export HTPASSWD_PATH="/usr/local/etc/htpasswd"
주의: 지정된 경로에 Nginx가 실제로 사용하는 .htpasswd 파일이 있어야 합니다. 쓰기 권한도 필요합니다.
3. 최초 관리자 계정 생성 (터미널)
VaultKeeper는 자체 로그인 로직을 포함하지 않으며, Nginx auth_basic 모듈에 인증을 위임합니다. 웹 UI에 접근하려면 Nginx를 통해 인증할 수
있는 계정이 필요합니다. 최초 관리자 계정은 VaultKeeper가 실행되기 전에 터미널에서 htpasswd 명령어를 사용하여 생성해야 합니다.
.htpasswd 파일이 아직 없는 경우:
htpasswd -c /etc/nginx/.htpasswd adminuser
adminuser를 원하는 관리자 사용자 이름으로 바꾸세요. 이 명령어는 /etc/nginx/.htpasswd 파일을 생성하고 adminuser를 추가합니다.
.htpasswd 파일이 이미 있는 경우 (새 사용자를 추가하거나 기존 사용자의 비밀번호를 변경):
htpasswd /etc/nginx/.htpasswd anotheruser
-c 옵션을 사용하면 기존 파일이 덮어쓰여지므로 절대 이미 존재하는 파일에 -c 옵션을 다시 사용하지 마세요.
4. VaultKeeper 실행
환경 변수 설정 및 .htpasswd 파일 준비가 완료되면, 빌드된 VaultKeeper 바이너리를 실행합니다.
./vaultkeeper
기본적으로 VaultKeeper는 8000번 포트에서 수신 대기합니다.
5. Nginx 설정 예시
Nginx의 auth_basic 모듈 뒤에서 VaultKeeper를 실행하여 접근을 제한할 수 있습니다. 다음은 Nginx 설정 파일 (/etc/nginx/nginx.conf 또는
conf.d 내부 파일)에 추가할 수 있는 location 블록 예시입니다.
이 설정은 /vaultkeeper/ 경로로 들어오는 요청을 VaultKeeper 백엔드(8000번 포트)로 프록시하고, auth_basic을 통해 .htpasswd 파일로 인증을
요구합니다.
server {
listen 80;
server_name your_domain.com; # 도메인 또는 IP 주소로 변경
location /vaultkeeper/ {
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/.htpasswd; # .htpasswd 파일 경로를 정확히 지정
proxy_pass http://127.0.0.1:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
}
# 다른 Nginx 설정...
}
Nginx 설정을 변경한 후에는 반드시 Nginx를 재로드하거나 재시작하여 변경사항을 적용해야 합니다.
sudo nginx -t # 설정 파일 유효성 검사
sudo systemctl reload nginx # Nginx 재로드
이제 http://your_domain.com/vaultkeeper/ (또는 http://your_server_ip/vaultkeeper/)로 접근하여 VaultKeeper 웹 UI를 사용할 수 있습니다.
접근 시 Nginx가 사용자 이름과 비밀번호를 요청할 것입니다.
▌ ▌ ▌ ▌ ▌ ▌ ▌ REPLACE
새로 생성된 파일을 확인하고 싶으시다면, 다음 명령어를 실행하여 바이너리를 빌드하고 서버를 시작할 수 있습니다:
```bash
go build -o vaultkeeper -ldflags "-s -w" main.go
export HTPASSWD_PATH="/tmp/.htpasswd" # 테스트용 임시 경로, 실제 경로로 변경 필요
htpasswd -c /tmp/.htpasswd adminuser # 최초 관리자 계정 생성
./vaultkeeper
index.html을 브라우저에서 열거나, Nginx를 설정한 후 http://localhost:8000 (또는 Nginx 프록시 설정에 따른 URL)으로 접속하여 프론트엔드를
확인할 수 있습니다.
Tokens: 3.3k sent, 5.0k received. Cost: $0.01 message, $0.01 session.
main.go
Create new file? (Y)es/(N)o [Yes]: y
index.html
Create new file? (Y)es/(N)o [Yes]: y
deployment.md
Create new file? (Y)es/(N)o [Yes]: y
Applied edit to main.go
Applied edit to index.html
Applied edit to deployment.md
Commit bd414ad feat: 새로운 htpasswd 관리 애플리케이션 VaultKeeper 구현
go build -o vaultkeeper -ldflags "-s -w" main.go
export HTPASSWD_PATH="/tmp/.htpasswd" # 테스트용 임시 경로, 실제 경로로 변경 필요
htpasswd -c /tmp/.htpasswd adminuser # 최초 관리자 계정 생성
./vaultkeeper
Run shell commands? (Y)es/(N)o/(D)on't ask again [Yes]: y
Running go build -o vaultkeeper -ldflags "-s -w" main.go
Running export HTPASSWD_PATH="/tmp/.htpasswd" # 테스트용 임시 경로, 실제 경로로 변경 필요
'export'() Ǵ ܺ , ִ α׷, Ǵ
ġ ƴմϴ.
Running htpasswd -c /tmp/.htpasswd adminuser # 최초 관리자 계정 생성
'htpasswd'() Ǵ ܺ , ִ α׷, Ǵ
ġ ƴմϴ.
Running ./vaultkeeper
'.'() Ǵ ܺ , ִ α׷, Ǵ
ġ ƴմϴ.
Add command output to the chat? (Y)es/(N)o/(D)on't ask again [Yes]: y
Added 11 lines of output to the chat.
You can use /undo to undo and discard each aider commit.
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
deployment.md doc\vault-keeper 상세 설계도.txt doc\지시서.txt index.html main.go
> /ls
Files in chat:
deployment.md
index.html
main.go
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
deployment.md doc\vault-keeper 상세 설계도.txt doc\지시서.txt index.html main.go
>
@@ -0,0 +1,69 @@
---
id: "Aider 프로젝트 생성 20260413"
created: "2026-04-13 13:31"
tags:
---
새로운 프로젝트를 만든다고 가정
만들고싶은 디렉토리에서
### 1단계: 프로젝트 폴더 생성 및 이동
먼저 프로젝트별로 방을 나눠야 합니다.
```bash
mkdir my-new-project # 원하는 프로젝트 이름으로 생성
cd my-new-project
```
### 2단계: Git 초기화 (매우 중요)
에이더는 작업 내용을 자동으로 기록하고 커밋하기 위해 **Git**을 기반으로 움직입니다. 폴더를 만들자마자 가장 먼저 해야 할 일입니다.
```bash
git init
```
- 이 과정을 거쳐야 나중에 에이더가 사고를 쳐도 `git checkout` 등으로 안전하게 코드를 복구할 수 있습니다.
### 3단계: API 키 및 기본 설정 확인
매번 키를 입력하기 귀찮다면, 아까 논의했던 대로 **환경 변수**나 **설정 파일**이 준비되어 있는지 확인합니다.
- 이미 윈도우 환경 변수에 `GEMINI_API_KEY`를 등록하셨다면 이 단계는 건너뛰셔도 됩니다.
- 만약 특정 모델을 고정하고 싶다면 이 폴더에 `.aider.conf.yml` 파일을 만드세요.
### 4단계: 에이더(Aider) 실행
이제 준비가 끝났습니다. 에이더를 깨웁니다.
```bash
aider
```
- 만약 별칭(alias)이나 설정 파일을 만들지 않았다면 직접 모델명을 적어줍니다: `aider --model gemini/gemini-1.5-pro`
- pro는 비싸니까 그냥 flash로 하는게 정신건강과 지갑건강상 좋음
### 5단계: 첫 번째 파일 생성 요청
에이더가 실행되면 AI에게 첫 작업을 시켜보세요.
- 예: `Hello! 파이썬으로 간단한 계산기 로직이 담긴 calculator.py 파일을 만들어줘.`
### 최종 결과물 구조
이렇게 순서대로 진행하면 폴더 안이 아래와 같이 예쁘게 정렬됩니다.
```cmd
workspaces/
└── my-new-project/
├── .git/ (2단계에서 생성)
├── .aider.chat.history.md (4단계 실행 후 생성)
├── .aider.tags.cache.v4/ (4단계 실행 후 생성)
└── calculator.py (5단계에서 생성됨)
```
**꿀팁:** 작업을 하다가 마음에 안 들면 에이더 채팅창 안에서 `/undo`를 입력하세요. 그러면 에이더가 방금 한 커밋을 취소하고 코드를 직전 상태로 돌려놓습니다.
@@ -0,0 +1,66 @@
---
id: "MariaDB vs PostgreSql 20260421"
created: "2026-04-21 16:43"
tags:
---
이 두 DB는 현재 오픈소스 RDBMS 시장의 양대 산맥입니다. **MariaDB**가 MySQL의 친숙함을 계승하며 실용성에 집중한다면, **PostgreSQL**은 데이터 무결성과 고급 기능을 중시하는 학구적이고 정교한 DB라고 할 수 있습니다.
## 1. 철학 및 개발 배경
|**구분**|**MariaDB**|**PostgreSQL**|
|---|---|---|
|**태생**|MySQL에서 분차(Fork)되어 나온 실용주의 DB|학계에서 시작된 객체-관계형(ORDBMS) 표준 지향 DB|
|**슬로건**|"MySQL보다 빠르고 더 많은 기능을 무료로"|"세계에서 가장 진보된 오픈소스 관계형 데이터베이스"|
|**라이선스**|GPL v2 (비즈니스 시 소스 공개 의무 주의)|PostgreSQL 라이선스 (BSD/MIT와 유사, 매우 자유로움)|
## 2. 기술적 핵심 차이
### 🚩 SQL 표준 및 복잡한 쿼리 처리
- **PostgreSQL:** SQL 표준 준수율이 매우 높습니다. 복잡한 조인(Join), 재귀 쿼리(Recursive CTE), 윈도우 함수 성능이 뛰어나며, 분석용 쿼리 최적화가 강력합니다. (MSSQL과 사용감이 비슷합니다.)
- **MariaDB:** MySQL의 문법을 따르며, 단순한 CRUD 성능에 최적화되어 있습니다. 최근 버전에서 CTE나 윈도우 함수를 지원하지만, 복잡한 비즈니스 로직 처리에서는 PostgreSQL에 비해 최적화가 덜 정교할 수 있습니다.
### 🚩 데이터 타입과 확장성
- **PostgreSQL:** 독보적입니다. JSONB(바이너리 JSON) 지원으로 NoSQL처럼 쓸 수 있고, 지리 정보(PostGIS), 전문 검색(Full-text search) 등이 내장되어 있습니다. 사용자 정의 타입도 만들 수 있습니다.
- **MariaDB:** 동적 컬럼(Dynamic Columns) 기능을 통해 비정형 데이터를 다루지만, PostgreSQL의 JSONB 성능에는 미치지 못합니다. 대신 다양한 스토리지 엔진(InnoDB, Aria, ColumnStore 등)을 용도에 맞게 선택할 수 있는 유연성이 있습니다.
### 🚩 동시성 제어 (MVCC)
- **PostgreSQL:** 데이터 쓰기 중에도 읽기가 차단되지 않는 MVCC(Multi-Version Concurrency Control) 방식이 매우 세련되어 있어, 동시 접속자가 많은 대형 시스템에 유리합니다.
- **MariaDB:** 스토리지 엔진(주로 InnoDB) 수준에서 MVCC를 지원하며, 읽기 위주의 서비스에서 가볍고 빠르게 동작합니다.
## 3. 상세 비교 테이블
| **항목** | **MariaDB** | **PostgreSQL** |
| ------------------- | ---------------------------- | ------------------------------------------- |
| **주요 용도** | 웹 서비스, CMS(워드프레스 등), 단순 CRUD | 복잡한 데이터 분석, 금융 시스템, GIS 서비스 |
| **성능 특징** | 읽기(Read) 성능이 매우 빠름 | 복잡한 쓰기(Write) 및 대량 데이터 처리에 강함 |
| **복제(Replication)** | 마스터-슬레이브 방식이 매우 쉽고 직관적 | 논리적 복제 등 다양한 방식 지원 (설정은 좀 더 복잡) |
| **저장 프로시저** | 지원 (MySQL 문법) | 강력 지원 (PL/pgSQL, Python, Perl 등 지원) |
| **인덱싱** | 기본 B-Tree 위주 | B-Tree, Hash, GiST, SP-GiST, GIN, BRIN 등 다양 |
## 4. 어떤 것을 선택해야 할까?
### 🐧 MariaDB를 선택할 때
- 기존에 MySQL을 써본 적이 있어 익숙함이 중요할 때.
- 커뮤니티나 자료가 많은 것을 선호할 때.
- 단순한 웹 게시판이나 가벼운 데이터 저장용일 때.
- PHP나 구형 웹 스택과의 호환성이 중요할 때.
### 🐘 PostgreSQL을 선택할 때
- **MSSQL 2019의 고급 기능을 오픈소스에서도 누리고 싶을 때.**
- 데이터 정합성이 무엇보다 중요한 금융, 결제, 정산 관련 프로젝트일 때.
- JSON 데이터를 빈번하게 다루면서 속도도 포기할 수 없을 때.
- 프로젝트가 커질 것을 대비해 확장성이 뛰어난 DB가 필요할 때.
+64
View File
@@ -0,0 +1,64 @@
---
id: RDBMS vs NoSQL 20260420
created: 2026-04-20 08:43
tags:
---
DB에는 크게 [[RDBMS]]와 NoSQL 두가지의 범주가 있음.
둘 다 데이터베이스를 구축하기위한 방법론의 개념이고 상호간에 차이점들이 존재한다.
하지만 데이터베이스라면 당연히 [[데이터 정합성(Data Consistency)]]을 지키는것이 최우선 목표이다.
NoSQL이 [[데이터 정합성(Data Consistency)]]을 지키지 않는 것은 절대 아니다.
[[데이터 정합성(Data Consistency)]]을 지키지 않는 건 데이터베이스로서 가치가 없다.
이 둘의 가장 큰 차이점은 데이터의 중복을 어느 정도 허용할 것인가? 라고 생각된다.
![[NoSQL#데이터 중복(Data Redundancy)]]
## NoSQL이 중복을 허용하는 이유
### **JOIN 연산의 배제**
NoSQL은 수평적 확장을 위해 데이터를 여러 서버에 분산 저장합니다. 이때 여러 서버에 흩어진 데이터를 JOIN 하는 작업은 성능에 막대한 지장을 줍니다. 이를 피하기 위해 필요한 데이터를 한 곳에 모아(중복 저장) 한 번의 쿼리로 읽어오도록 설계합니다.
### **읽기 성능 극대화**
사용자가 화면을 볼 때 필요한 모든 정보를 하나의 'Document'나 'Row'에 다 담아두면, 여러 테이블을 뒤질 필요 없이 즉시 응답할 수 있습니다.
## 데이터 중복으로 인한 기회비용
중복을 허용하면 얻는 것도 있지만, 반드시 대가를 치러야 합니다.
- **데이터 수정의 복잡성 (Update Anomaly):** 사용자 이름이 바뀌면, 그 이름이 중복 저장된 수천 개의 게시글 데이터를 모두 업데이트해야 합니다. 이때 일부가 누락되면 데이터 정합성이 깨집니다.
- **저장 공간 증가:** 동일한 데이터가 반복 저장되므로 저장 용량을 더 많이 차지합니다. 하지만 현대의 클라우드 인프라에서는 저장 비용보다 컴퓨팅 성능(속도) 비용이 더 중요하므로 대개 용납됩니다.
- **결과적 일관성 (Eventual Consistency):** 모든 중복 데이터를 즉시 수정하는 것이 어렵기 때문에, "잠시 동안은 데이터가 다를 수 있지만 결국에는 같아진다"는 원칙을 따르게 됩니다.
## RDBMS vs NoSQL: 언제 무엇을 쓸까?
어느 하나가 절대적으로 우월한 것이 아니라, 서비스의 성격에 맞춰 선택하거나 두 가지를 혼합해서 사용합니다.
### **RDBMS를 써야 할 때**
- 데이터 구조가 명확하고 변경될 일이 적을 때
- **금융 시스템**처럼 [[데이터 정합성(Data Consistency)]]과 [[트랜잭션(Transaction)|트랜젝션]]([[ACID]])이 매우 중요할 때
- 복잡한 쿼리와 JOIN 연산이 자주 필요할 때
### **NoSQL을 써야 할 때**
- 데이터 구조가 확정되지 않았거나 자주 변경될 때
- **빅데이터, 로그 관리, 실시간 메신저**처럼 막대한 양의 데이터를 빠르게 처리해야 할 때
- [[데이터 정합성(Data Consistency)]]보다는 서비스의 [[가용성(Availability)]](항상 접속 가능함)이 더 중요할 때
[[NoSQL]]은 "데이터가 조금 틀려도 괜찮으니, 절대 죽지 않고 엄청나게 빠른 시스템"을 만들 때 씁니다. 반면 [[RDBMS]]는 "성능이 조금 답답하더라도, 데이터는 단 1원, 1글자도 틀리면 안 되는 시스템"에 씁니다.
> [!warning] 그렇다고 해서 NoSQL이 데이터정합성이 안맞단건 절대 아님
> 실시간으로 정합성을 맞춰주느냐? (RDBMS), 순간적으로는 안맞을 수 있지만 결과적으론 정합성이 맞다 (NoSQL)
@@ -0,0 +1,20 @@
---
id: RDBMS사용 시 주의점들 20260420
created: 2026-04-20 10:55
tags:
---
> [!question]
> Q. 관계형db에서는 테이블간의 관계를 여러테이블을 조인하지 않게끔 적절히 분산시키면서 인덱스를 잘 거는게 엄청 중요하겠네?
> A. 정확하게 짚으셨습니다! 관계형 데이터베이스(RDBMS) 운영의 핵심이자 개발자의 실력이 가장 극명하게 드러나는 지점이 바로 그 **'조인(JOIN) 최소화'와 '인덱스(Index) 전략'**입니다.
## 조인 최소화: "관계는 유지하되, 데이터는 뭉친다"
관계형 DB라고 해서 무조건 모든 데이터를 쪼개는 게 정답은 아닙니다. 성능을 위해 의도적으로 정합성을 조금 희생하는 기법들이 필요합니다.
과도하게 데이터를 쪼개놓을 경우 데이터 조인이 많이 발생될 수 밖에 없는데 데이터 조인은 어느정도 느릴 수 밖에 없다.
- [[데이터 조인이 느린 이유]]
결국 RDBMS 설계의 꽃은 **인덱스를 활용해 '수학적인 탐색 비용'을 얼마나 낮게 유지하느냐**에 달려 있습니다.
하지만 아무리 인덱스를 잘 설정해도 조인해야 할 테이블이 5개, 10개씩 늘어나면 성능은 떨어질 수밖에 없습니다.
- **Nested Loop의 누적:** 조인이 중첩될수록 내부 루프의 횟수가 곱하기로 늘어납니다.
그래서 적당한 데이터 분산이 중요합니다.
@@ -0,0 +1,32 @@
---
id: 어플리케이션(서비스) 실행 20260413
created: 2026-04-13 14:57
tags:
---
> [!question]
> Q. 리눅스에서 서비스나 어플리케이션 새로 실행할 때 계정을 따로 생성해서 그 계정으로 실행하는 이유가 뭐야?
> A. 리눅스에서 Gitea, Jenkins와 같은 서비스를 전용 계정(Service Account)으로 실행하는 가장 큰 이유는 **보안**과 **시스템 안정성** 때문입니다. 만약 루트(root) 권한으로 서비스를 실행했다가 해당 서비스가 해킹당하면 시스템 전체가 장악될 수 있기 때문이죠.
### 1. 최소 권한의 원칙 (Principle of Least Privilege)
가장 핵심적인 개념입니다. 특정 서비스가 작동하는 데 필요한 **최소한의 권한**만 부여하는 것입니다.
- **격리:** 전용 계정을 만들면 해당 계정은 자신의 홈 디렉터리와 서비스 관련 파일에만 접근할 수 있습니다.
- **피해 최소화:** 서비스에 취약점이 있어 해커가 침입하더라도, 해커는 해당 계정의 권한만 갖게 됩니다. 시스템 설정 파일을 수정하거나 다른 사용자의 데이터를 훔쳐보는 것이 차단됩니다.
### 2. 시스템 자원 및 프로세스 관리
전용 계정을 사용하면 관리 효율성이 높아집니다.
- **자원 제한:** 특정 계정이 사용할 수 있는 CPU, 메모리, 파일 열기 개수 등을 제한(ulimit 등)하여 하나의 서비스가 시스템 전체 자원을 독점하는 것을 방지할 수 있습니다.
- **로그 및 모니터링:** 어떤 프로세스가 어떤 계정으로 실행 중인지 명확하므로, 로그를 분석하거나 문제를 추적할 때 훨씬 수월합니다.
### 3. 보안 사고 발생 시 대응
- **계정 차단:** 특정 서비스 계정에서 이상 징후가 발견되면 시스템 전체를 내릴 필요 없이 해당 계정만 잠그거나 권한을 박탈하여 즉각 대응할 수 있습니다.
- **비로그인 계정:** 보통 서비스용 계정은 `nologin`이나 `false` 쉘을 부여하여, 사람이 직접 SSH 등으로 로그인하는 것을 막아둡니다. 이는 외부 공격자가 해당 계정으로 서버에 직접 접속하는 경로를 원천 차단하는 효과가 있습니다.
@@ -0,0 +1,21 @@
---
id: 라우팅 20260428
created: 2026-04-28 14:14
tags:
---
PC1과 PC2 사이에 랜선으로 직접 연결하여 통신을 하게 할 수 있음.
단, 연결하자마자 곧바로 되는 건 아니고 몇 가지 설정을 해야 함.
일단 이 랜선이 꽂힌 랜포트에 대해 설정을 해주어야 함.
이 연결에는 [[DHCP(Dynamic Host Configuration Protocol)]]서버가 없기 때문에 자동으로 설정할 수 없음.
[[IP]]주소, [[서브넷 마스크(Subnet Mask)]], 기본[[게이트웨이(Gateway)]]를 설정한다.
그리고 PC에 더이상의 추가적인 통신연결이 없을경우에는 별도의 설정없이 곧바로 통신이 가능해진다.
"어디로든 나가야 하면 일단 이 랜선으로 나가자"라는 기본 규칙(Default Route)이 아주 단순하게 작동하게됨.
만약 둘 이상의 통신연결이 있을 경우 [[라우팅 테이블(Routing Table)]]에 등록이 되어있어야 통신이 가능해진다.
[[라우팅 테이블(Routing Table)]]은 통신연결 시도측, 받는측 양측 모두에 상대방이 등록되어있어야 한다.
> [!info] **자동 생성:**
> 윈도우나 리눅스 같은 운영체제는 랜카드에 IP(`192.168.100.x`)를 설정하는 순간, **"이 대역으로 가는 길은 이 랜카드다"라는 규칙을 라우팅 테이블에 자동으로 추가**합니다.
@@ -0,0 +1,9 @@
---
id: "RS-232 20260504"
created: "2026-05-04 09:20"
tags:
---
RS-232는 [[시리얼 통신(Serial Communication)]] 방식을 구현하기 위해 정해진 구체적인 표준임.
Serial이 교통수단이라면 RS-232는 자동차라고 볼 수 있음.
@@ -0,0 +1,74 @@
---
id: "Gitea & Quartz 배포 자동화 트러블슈팅 20260401"
created: "2026-04-01 13:23"
tags:
---
## 🛠️ Gitea & Quartz 배포 자동화 트러블슈팅 기록
### 1. 문제의 발단: Gitea 경로 변경 (`/git/` 누락)
기존에 설정된 Gitea의 접속 주소가 `/git/` 경로를 포함하고 있었으나, 서버 재설정 과정에서 이 경로가 빠지게 되었습니다. 이로 인해 연쇄적인 문제가 발생했습니다.
- **현상:** Gitea 웹훅(Webhook)이 서버의 수신기(`webhook` 서비스)에 신호를 보내지 못함.
- **원인:** Gitea 내부 리포지토리 설정과 외부 호출 URL이 일치하지 않아 404 에러 또는 연결 거부 발생.
---
### 2. 주요 장애 요인 및 해결 과정
#### 📂 [Level 1] 서비스 실행 실패 (ConditionPathExists)
- **에러:** `webhook.service was skipped because of an unmet condition check`.
- **원인:** 기본 설치된 `webhook` 서비스가 `/etc/webhook.conf` 파일이 있어야만 작동하도록 설정됨.
- **해결:** 서비스 파일(`systemd`)을 수정하여 해당 조건을 삭제하고, 우리가 직접 만든 `hooks.json`을 바라보도록 설정함.
#### 🔑 [Level 2] 인증 에러 (no source for value retrieval)
- **에러:** `500 | error evaluating hook: no source for value retrieval`.
- **원인:** `hooks.json`에는 비밀번호(Secret) 검사 로직이 있는데, Gitea 웹훅 설정에는 비밀번호가 비어 있거나 헤더 이름이 일치하지 않음.
- **해결:** 보안을 위해 Gitea와 서버 양쪽에 동일한 비밀번호를 설정하거나, 테스트를 위해 `trigger-rule`을 삭제하여 문턱을 낮춤.
#### 🚀 [Level 3] 스크립트 실행 환경 문제 (npx & node)
- **에러:** `npx: command not found` 또는 `node: No such file or directory`.
- **원인:** `systemd` 서비스는 사용자 터미널 환경변수(`PATH`)를 읽지 못해 NVM에 설치된 Node.js를 찾지 못함.
- **해결:** `deploy.sh` 상단에 Node.js 실행 파일이 있는 **절대 경로**를 `export PATH`로 직접 주입함.
#### 📉 [Level 4] 자원 부족 및 서버 경직 (kswapd0)
- **현상:** 빌드 시작 시 서버가 급격히 느려지며 접속이 끊김.
- **원인:** Quartz 빌드는 CPU와 메모리를 많이 사용하는데, 512MB~1GB 수준의 낮은 메모리로 인해 스왑(Swap) 현상이 심화됨.
- **해결:** 1. **Swap 확장:** 기존 스왑 파일을 **2GB**로 대폭 확장하여 물리 메모리 부족에 대비함.
2. **우선순위 조정:** 빌드 명령어 앞에 **`nice -n 19`**를 붙여 다른 서비스(Nginx, Gitea)에 영향을 주지 않도록 최적화함.
---
### 3. 최종 자동화 아키텍처 (The "Tight" Setup)
|**구성 요소**|**역할**|**최적화 포인트**|
|---|---|---|
|**Gitea**|소스 관리 및 트리거|`/git/` 경로를 제외한 클린 URL 적용|
|**Webhook**|신호 수신기 (Port 29292)|전용 서비스 등록 및 자동 시작 설정|
|**deploy.sh**|배포 스크립트|**절대 경로 사용**, **로그 파일 기록**, **우선순위 최저 설정**|
|**Swap File**|가상 메모리 (2GB)|`/etc/fstab` 등록을 통한 영구 적용|
---
### 📝 교훈
> **경로(Path)와 환경(Environment)은 언제나 명시적이어야 한다.** > 공짜 서버일수록 수동으로 자원을 배분하고 우선순위를 관리하는 **빡빡한 최적화**가 시스템의 생명줄이 된다.
+38
View File
@@ -0,0 +1,38 @@
![[Pasted image 20260407165750.png]]
/home/dihwang/webhook/deploy.sh
```bash
#!/bin/bash
if [ -f /tmp/quartz.lock ]; then exit; fi
# 1. Node와 npx가 있는 경로를 강제로 주입 (v22.22.2 기준)
export PATH=$PATH:/home/dihwang/.nvm/versions/node/v22.22.2/bin
# 2. 작업 디렉토리로 이동
cd /home/dihwang/white-smith-blog/content || exit
# 3. 최신 데이터 동기화
git pull origin master
cd ..
# 4. Quartz 빌드 (이제 npx가 node를 잘 찾을 겁니다)
nice -n 19 npx quartz build
echo "Quartz Build Completed at $(date)"
```
/home/dihwang/webhook/hook.json
```json
[
{
"id": "quartz-deploy",
"execute-command": "/home/dihwang/webhook/deploy.sh",
"command-working-directory": "/home/dihwang/white-smith-blog",
"response-message": "Deploying Quartz...",
}
]
```
@@ -0,0 +1,239 @@
---
id: "gitea, qurtz 설치 20260331"
created: "2026-03-31 10:41"
tags:
---
# 🌐 개인 서버(GCP) 기반 Gitea & HTTPS 구축 가이드
본 문서는 Ubuntu 25.xx 환경에서 가벼운 Git 서비스인 **Gitea**를 설치하고, **Nginx**와 **DuckDNS**를 이용해 **HTTPS** 보안 접속을 구현한 과정을 정리합니다.
## 1. 서버 기본 환경 및 Gitea 설치
가장 가벼운 바이너리 실행 방식으로 Gitea를 설치하고 전용 사용자를 생성했습니다.
- **OS:** Ubuntu 25.xx (GCP Instance)
- **사용자 생성:** `git` 시스템 사용자 생성 (`/home/git`)
- **DB:** SQLite3 (경량화를 위해 선택)
- **바이너리 경로:** `/usr/local/bin/gitea`
- **데이터 경로:** `/var/lib/gitea`
---
## 2. 도메인 및 네트워크 설정
외부 접속을 위해 무료 DDNS 주소를 확보하고 클라우드 방화벽을 조정했습니다.
- **도메인:** `white-smith.duckdns.org` (GCP 외부 IP 연결 완료)
- **포트 개방 (GCP & UFW):**
- `22/tcp`: SSH 접속 (PuTTY, SFTP)
- `80/tcp`: HTTP (인증서 발급 및 리다이렉트용)
- `443/tcp`: HTTPS (실제 서비스용)
- **보안 조치:** 불필요한 포트(20, 21, 3389 등) 차단 및 Gitea 직접 포트(3000)는 Nginx를 거치도록 외부 차단 권장.
---
## 3. HTTPS & Nginx 리버스 프록시
Nginx를 앞단에 세워 보안을 강화하고 도메인 기반 접속을 구현했습니다.
### SSL 인증서 발급
`Certbot`을 이용해 **Let's Encrypt** 무료 SSL 인증서를 발급받았습니다.
Bash
```
sudo certbot --nginx -d white-smith.duckdns.org
```
### Nginx 최종 설정 (`/etc/nginx/sites-available/gitea`)
IP 접속 시 도메인으로 자동 전환되도록 설정했습니다.
Nginx
```
# 1. HTTP -> HTTPS 리다이렉트 (IP 포함)
server {
listen 80;
server_name 34.19.79.94 white-smith.duckdns.org;
return 301 https://white-smith.duckdns.org$request_uri;
}
# 2. HTTPS 서비스 블록
server {
listen 443 ssl;
server_name white-smith.duckdns.org;
# SSL 인증서 경로 (Certbot 관리)
ssl_certificate /etc/letsencrypt/live/white-smith.duckdns.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/white-smith.duckdns.org/privkey.pem;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
---
## 4. Gitea 내부 설정 업데이트
HTTPS 환경에 맞춰 `/etc/gitea/app.ini` 파일을 `vi`로 수정했습니다.
- **ROOT_URL:** `https://white-smith.duckdns.org/`
- **DOMAIN:** `white-smith.duckdns.org`
- **SSH_DOMAIN:** `white-smith.duckdns.org`
---
## 5. 현재 상태 요약
1. **HTTPS 접속:** `https://white-smith.duckdns.org`로 안전하게 접속 가능.
2. **IP 리다이렉트:** `34.19.79.94` 입력 시 자동으로 도메인 주소로 전환됨.
3. **파일 전송:** 포트 22번을 통해 SFTP 방식으로 안전하게 파일 관리 중.
# 🚀 Gitea & Quartz 도메인 통합 및 서버 설정 가이드
본 가이드는 단일 메인 도메인(`white-smith.duckdns.org`)에서 **Quartz(블로그)**와 **Gitea(Git 서비스)**를 충돌 없이 운영하기 위한 설정법을 다룹니다.
---
## 1. 서비스 주소 설계
동일 도메인 점유 문제를 해결하기 위해 **하위 경로(Path)** 방식으로 서비스를 분리했습니다.
- **Quartz (메인 블로그):** `https://white-smith.duckdns.org/`
- **Gitea (코드 저장소):** `https://white-smith.duckdns.org/git/`
---
## 2. Gitea 설정 수정 (`app.ini`)
Gitea가 `/git/` 경로를 인식하도록 내부 설정을 변경해야 합니다.
- **파일 위치:** `/var/lib/gitea/custom/conf/app.ini` (또는 설치 경로 내 `custom/conf/app.ini`)
- **수정 내용:**
Ini, TOML
```
[server]
ROOT_URL = https://white-smith.duckdns.org/git/
LOCAL_ROOT_URL = http://127.0.0.1:3000/
HTTP_ADDR = 127.0.0.1
HTTP_PORT = 3000
```
> **Tip:** `LOCAL_ROOT_URL`을 추가해야 내부 리다이렉션 시 404 오류를 방지할 수 있습니다.
---
## 3. Nginx 역방향 프록시 설정
`/etc/nginx/sites-available/gitea` 파일을 수정하여 두 서비스를 통합합니다.
Nginx
```
server {
listen 80;
server_name 34.19.79.94 white-smith.duckdns.org;
return 301 https://white-smith.duckdns.org$request_uri;
}
server {
listen 443 ssl;
server_name white-smith.duckdns.org;
ssl_certificate /etc/letsencrypt/live/white-smith.duckdns.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/white-smith.duckdns.org/privkey.pem;
# [우선순위 1] Gitea 설정
location /git/ {
proxy_pass http://127.0.0.1:3000/; # 끝에 '/' 필수
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# [우선순위 2] Quartz 설정 (메인)
location / {
proxy_pass http://127.0.0.1:8080; # Quartz 실행 포트
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
---
## 4. 권한 관리 및 접근 (etc 경로)
시스템 설정 파일을 편리하게 수정하기 위해 권한을 조정하거나 도구를 활용합니다.
1. **권한 변경 (추천):**
Bash
```
sudo chown -R root:$(whoami) /etc/nginx
sudo chmod -R 775 /etc/nginx
```
2. **포트 확인 명령어:**
Bash
```
sudo ss -tulpn | grep node # Quartz 포트 확인
```
3. **서비스 재시작:**
Bash
```
sudo nginx -t && sudo systemctl restart nginx
sudo systemctl restart gitea
```
---
## 5. 핵심 주의사항
- **Nginx 경로 슬래시:** `proxy_pass http://127.0.0.1:3000/;` 처럼 끝에 `/`가 있어야 `/git/` 경로가 중복되지 않고 올바르게 전달됩니다.
- **Quartz 실행:** Quartz를 `npx quartz build --serve`로 띄운 경우 해당 포트(보통 8080)가 Nginx 설정과 일치해야 합니다.
- **보안:** `/etc` 전체 권한을 `777`로 바꾸지 마세요. 필요한 폴더만 그룹 권한을 부여하는 것이 안전합니다.
+51
View File
@@ -0,0 +1,51 @@
---
id: "kui-vault 20260401"
created: "2026-04-01 14:59"
tags:
---
**Google Cloud Platform(GCP)** 를 사용해서 구축함.
### 컴퓨팅 (Compute)
사용자가 코드를 실행할 공간을 제공합니다.
- **Compute Engine (GCE):** 가상 머신(VM)을 직접 생성하고 관리하는 IaaS 서비스입니다. (AWS의 EC2와 유사)
GCE를 한대 빌려서 구축하였음
![[Pasted image 20260401150036.png]]
원래는 퍼블릭하게 공개되면 안되는 자료들을 블러처리해주는 서버를 만들려고 (kui-veil) 확보한 서버였음
![[Pasted image 20260401163033.png]]
## 🖥️ Server Information: **kui-veil**
#### 1. 기본 호스트 정보
| **항목** | **내용** |
| ---------------------- | ---------------------------------- |
| **Static Hostname** | (unset) |
| **Transient Hostname** | **kui-veil** |
| **Machine ID** | `b723eb615fc74ff3ac0de806f770d293` |
| **Boot ID** | `2ce0c86e6d704281a6c28054cc4eba0e` |
#### 2. 운영체제 및 커널
|**항목**|**내용**|
|---|---|
|**Operating System**|**Ubuntu 25.10**|
|**Kernel Version**|**Linux 6.17.0-1007-gcp**|
|**Architecture**|**x86-64** (64비트)|
#### 3. 하드웨어 및 가상화 (GCP)
|**항목**|**내용**|
|---|---|
|**Virtualization**|**google** (Google Compute Engine)|
|**Hardware Vendor**|**Google**|
|**Chassis**|**vm** 🖴|
|**Firmware Date**|2026-02-12 (약 1.5개월 전)|
### 💡 주요 특징 메모
- **최신 버전 사용 중:** 현재 **Ubuntu 25.10**을 사용 중이시네요. 이는 매우 최신 배포판으로, 최신 커널 기능과 패키지들을 지원합니다.
- **GCP 최적화:** 커널 이름에 `-gcp`가 붙어 있어, 구글 클라우드 인프라에 최적화된 환경에서 동작하고 있음을 알 수 있습니다.
- **상태 요약:** 펌웨어 날짜가 2026년 2월인 것으로 보아, 시스템이 최근에 업데이트되었거나 생성된 아주 따끈따끈한 환경입니다!
@@ -0,0 +1,63 @@
---
id: "worker server spec 20260414"
created: "2026-04-14 13:28"
tags:
---
ubuntu@instance-20260414-0951:~$ **lscpu**
Architecture: aarch64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 4
On-line CPU(s) list: 0-3
Vendor ID: ARM
Model name: Neoverse-N1
Model: 1
Thread(s) per core: 1
Core(s) per socket: 4
Socket(s): 1
Stepping: r3p1
BogoMIPS: 50.00
Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics
fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-3
Vulnerabilities:
Gather data sampling: Not affected
Ghostwrite: Not affected
Indirect target selection: Not affected
Itlb multihit: Not affected
L1tf: Not affected
Mds: Not affected
Meltdown: Not affected
Mmio stale data: Not affected
Old microcode: Not affected
Reg file data sampling: Not affected
Retbleed: Not affected
Spec rstack overflow: Not affected
Spec store bypass: Mitigation; Speculative Store Bypass disabled via p
rctl
Spectre v1: Mitigation; __user pointer sanitization
Spectre v2: Mitigation; CSV2, BHB
Srbds: Not affected
Tsa: Not affected
Tsx async abort: Not affected
Vmscape: Not affected
ubuntu@instance-20260414-0951:~$ **free -h**
total used free shared buff/cache available
Mem: 23Gi 691Mi 21Gi 5.4Mi 1.9Gi 22Gi
Swap: 0B 0B 0B
ubuntu@instance-20260414-0951:~$ **df -h**
Filesystem Size Used Avail Use% Mounted on
tmpfs 2.4G 1.5M 2.4G 1% /run
efivarfs 256K 14K 243K 6% /sys/firmware/efi/efivars
/dev/sda1 193G 3.1G 190G 2% /
tmpfs 12G 0 12G 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
/dev/sda16 891M 94M 735M 12% /boot
/dev/sda15 98M 6.4M 92M 7% /boot/efi
tmpfs 2.4G 12K 2.4G 1% /run/user/1001
+45
View File
@@ -0,0 +1,45 @@
---
id: 전표처리 20260413
created: 2026-04-13 08:42
tags:
---
![[전표#📑 개념]]
회사 법인카드 사용하고나서 [[분개전표]] 처리를 해야한다. ([[전표]])
돈을 얼마나 썼는지, 왜 썼는지에 대한 상세사항정리 정도로 생각하면 됨.
> [!warning]
**분개전표 입력전에 법인카드사용내역회계전표생성(개인) 부터 먼저해야함.**
여기에 등록하고 승인나면 그때 자동으로 입력된 전표를 처리해야 함.
![[Pasted image 20260427095731.png]]
위 화면에선 특별히 추가할 거 없이 결재상신 하면 됨
1. 30만원 이하  PM -> 팀장 -> 사업부장, 합의 문새별 -> 김성민 입니다. (PM이 없는 경우나 PM이 팀장인 경우는 PM 생략)
결제 상세는 [[법인카드 결제상신 경로]] 참고
> [!note] 부가세 여부
[[부가세여부]] 참고
업무용 택시비, 소모품비, 숙소비는 부가세여부 O
일반차량 유류비, 톨게이트비 부가세여부 X
> [!warning] 법인카드사용내역회계전표생성(개인) 부터 먼저 결제하지 않고 진행하면...
전표 유형이 법인카드사용내역전표(개인) 이 아니고 일반분개전표 로 진행이 되어버림..
![[Pasted image 20260427133315.png]]
ERP에서 ESS -> 회계 -> 전표입력 으로 가서 전표입력 하면 됨.
![[분개(分介)#예시 이미지]]
위와같은 형태로 입력한다.
0002 대변금액은 직접 적는게 아니고 0001 차변금액을 아래로 드래그해서 추가하면 됨.
여기서 전표를 추가하고 그룹웨어에서 결제 진행하면 됨.