iykyka/core-backend/main.go

187 lines
4.8 KiB
Go

package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"gorm.io/gorm"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
type signupRequest struct {
InviteCode string `json:"invite_code" binding:"required"`
DisplayName string `json:"display_name" binding:"required"`
}
type sendMessageRequest struct {
SenderID uint `json:"sender_id" binding:"required"`
Text string `json:"text" binding:"required"`
SenderMode SenderMode `json:"sender_mode"`
}
type draftMessageRequest struct {
ContextLines []string `json:"context_lines" binding:"required"`
StyleExamples []string `json:"style_examples"`
History []string `json:"history"`
K int `json:"k"`
}
func setupRouter(db *gorm.DB, relay *ConnectionManager, ai *AIServiceClient) *gin.Engine {
r := gin.Default()
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
r.POST("/auth/signup", func(c *gin.Context) {
var req signupRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
var existing User
if err := db.Where("invite_code = ?", req.InviteCode).First(&existing).Error; err == nil {
c.JSON(http.StatusConflict, gin.H{"detail": "invite_code already used"})
return
}
user := User{InviteCode: req.InviteCode, DisplayName: req.DisplayName}
if err := db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
return
}
db.Create(&TwinSettings{UserID: user.ID, AutonomyLevel: AutonomyL0})
c.JSON(http.StatusOK, gin.H{"id": user.ID, "display_name": user.DisplayName})
})
r.POST("/conversations/:id/messages", func(c *gin.Context) {
convID, ok := parseUintParam(c, "id")
if !ok {
return
}
var conversation Conversation
if err := db.First(&conversation, convID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "conversation not found"})
return
}
var req sendMessageRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
if req.SenderMode == "" {
req.SenderMode = SenderHuman
}
message := Message{
ConversationID: convID,
SenderID: req.SenderID,
SenderMode: req.SenderMode,
Text: req.Text,
}
if err := db.Create(&message).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
return
}
relay.broadcast(convID, gin.H{
"id": message.ID,
"sender_id": message.SenderID,
"sender_mode": message.SenderMode,
"text": message.Text,
})
c.JSON(http.StatusOK, gin.H{"id": message.ID})
})
r.POST("/conversations/:id/draft", func(c *gin.Context) {
convID, ok := parseUintParam(c, "id")
if !ok {
return
}
var conversation Conversation
if err := db.First(&conversation, convID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "conversation not found"})
return
}
var req draftMessageRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
if len(req.StyleExamples) == 0 && len(req.History) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "provide style_examples or history"})
return
}
// EscalationLog persistence (sender's post-hoc notification + undo
// trail) is a separate checklist item -- roadmap.md Phase 1 §2.2
// "사후 알림 + 되돌리기 로그 스키마/API". This endpoint only proxies
// to the AI service for now.
result, err := ai.requestDraft(draftRequest{
ContextLines: req.ContextLines,
StyleExamples: req.StyleExamples,
History: req.History,
K: req.K,
})
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": result.Status, "text": result.Text})
})
r.GET("/ws/conversations/:id", func(c *gin.Context) {
convID, ok := parseUintParam(c, "id")
if !ok {
return
}
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer conn.Close()
relay.add(convID, conn)
defer relay.remove(convID, conn)
for {
if _, _, err := conn.ReadMessage(); err != nil {
break
}
}
})
return r
}
func parseUintParam(c *gin.Context, name string) (uint, bool) {
id, err := strconv.ParseUint(c.Param(name), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": name + " must be a positive integer"})
return 0, false
}
return uint(id), true
}
func main() {
db := openDB()
relay := newConnectionManager()
ai := newAIServiceClient()
r := setupRouter(db, relay, ai)
r.Run(":8080")
}