728x90
SMALL

1. 서버인증 구현

옛날에 직접 서버를 만들었을 때는 서버에서 받은 유저ID를 전달하여 서버에서 어떤 유저가 요청하였는지 구분하고 처리하도록 구현하지만, 이번에는 JWT토큰을 이용하여 처리해보기로 하였습니다.

 

서버에서 JWT토큰을 사용하기 위해 JwtBearer 라이브러리를 추가하였습니다.

 

appsettings.json 파일에 JWT를 사용할 때 필요한 정보를 추가하였습니다.
나중에 다 만들고 테스트할 당시에 Key값이 짧아 토큰생성을 실패하여 에러가 발생하지 않을 정도로 길게 입력하였습니다.

"Jwt": {
  "Issuer": "taxi-game-3d-issuer",
  "Audience": "taxi-game-3d-audience",
  "Key": "!!taxi-game-3d-secret-key-1234567890!!",
  "Lifetime": 600
}

 

appsettings.json 에 입력된 정보를 이용하여 토큰을 생성하는 클래스들을 제작하였습니다.

public class JwtSettings
{
    public required string Issuer { get; set; }
    public required string Audience { get; set; }
    public required string Key { get; set; }
    public double Lifetime { get; set; }

    public SymmetricSecurityKey GenerateKey() =>
        new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Key));

    public static implicit operator TokenValidationParameters(JwtSettings settings) => new()
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidIssuer = settings.Issuer,
        ValidAudience = settings.Audience,
        IssuerSigningKey = settings.GenerateKey()
    };
}
public class TokenService
{
    readonly JwtSettings jwtSettings;

    public TokenService(IOptions<JwtSettings> options)
    {
        jwtSettings = options.Value;
    }

    public (string token, DateTime expireUtc) Generate(UserModel user)
    {
        var securityKey = jwtSettings.GenerateKey();
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
        var claims = new Claim[]
        {
            new(JwtRegisteredClaimNames.Sub, user.Id!),
            new(JwtRegisteredClaimNames.Email, user.Email)
        };
        var expires = DateTime.Now.AddSeconds(jwtSettings.Lifetime);
        var token = new JwtSecurityToken(
            jwtSettings.Issuer,
            jwtSettings.Audience,
            claims,
            expires: expires,
            signingCredentials: credentials
        );
        return (
            new JwtSecurityTokenHandler().WriteToken(token),
            expires.ToUniversalTime()
        );
    }
}

 

구현한 JwtSettings, TokenService를 서비스에 등록하였습니다.

builder.Services.Configure<JwtSettings>(
    builder.Configuration.GetSection("Jwt")
);
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = builder
            .Configuration
            .GetSection("Jwt")
            .Get<JwtSettings>()!;
    });

 

처음에는 편하게 기기ID를 계정으로 사용하려고 했지만 욕심이 생겨 이메일을 대신 사용해보기로 하였습니다.
이메일과 비밀번호를 받을 수 있도록 UserModel, UserRepository를 수정하였습니다.

public class UserModel
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string? Id { get; set; }
    public string? Nickname { get; set; }
    public required string Email { get; set; }
    public required string Password { get; set; }
    public long Coin { get; set; }
    public List<string>? Cars { get; set; }
    [BsonElement("CurrentCar")]
    [JsonPropertyName("CurrentCar")]
    public string? CurrentCarId { get; set; }
    [BsonElement("CurrentStage")]
    [JsonPropertyName("CurrentStage")]
    public int CurrentStageIndex { get; set; }
}
public class UserRepository
{
    readonly IMongoCollection<UserModel> users;

    public UserRepository(DatabaseContext context)
    {
        users = context.Users;
    }

    public async Task Create(UserModel model) =>
        await users.InsertOneAsync(model);

    public async Task<UserModel> Get(string id) =>
        await users.Find(e => e.Id == id).FirstOrDefaultAsync();

    public async Task<UserModel> FindByEmail(string email) =>
        await users.Find(e => e.Email == email).FirstOrDefaultAsync();

    public async Task Update(string id, UserModel model) =>
        await users.ReplaceOneAsync(e => e.Id == id, model);

    public async Task Delete(string id) =>
        await users.DeleteOneAsync(e => e.Id == id);
}

 

미리 만들어 두었던 AuthController에 로그인, 회원가입, 토큰갱신 기능을 구현하였습니다.
발급받은 토큰은 DB에 저장하도록 처리하지 않았기 때문에 로그아웃은 구현하지 않았습니다.

[Route("[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
    readonly UserRepository userRepository;
    readonly TokenService tokenService;

    public AuthController(UserRepository userRepository, TokenService tokenService)
    {
        this.userRepository = userRepository;
        this.tokenService = tokenService;
    }

    [HttpPost("LoginEmail")]
    [ProducesResponseType(typeof(LoginResponse), 200)]
    public async Task<ActionResult> LoginWithEmail([FromBody] LoginWithEmailRequest body)
    {
        var user = await userRepository.FindByEmail(body.Email);
        if (user == null)
            return NotFound();

        if (user.Password != body.Password)
            return Forbid();

        var token = tokenService.Generate(user);
        return Ok(new LoginResponse
        {
            Token = token.token,
            ExpireUtc = token.expireUtc
        });
    }

    [HttpPost("CreateEmail")]
    [ProducesResponseType(typeof(LoginResponse), 201)]
    public async Task<ActionResult> CreateWithEmail([FromBody] LoginWithEmailRequest body)
    {
        var user = await userRepository.FindByEmail(body.Email);
        if (user != null)
            return Conflict();

        if (body.Password.Length < 8)
            return Forbid();

        user = new()
        {
            Email = body.Email,
            Password = body.Password
        };
        await userRepository.Create(user);

        var token = tokenService.Generate(user);
        return StatusCode(StatusCodes.Status201Created, new LoginResponse
        {
            Token = token.token,
            ExpireUtc = token.expireUtc
        });
    }

    [HttpGet("RefreshToken")]
    [ProducesResponseType(typeof(LoginResponse), 200)]
    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    public async Task<ActionResult> RefreshToken()
    {
        var userId = ClaimHelper.FindNameIdentifier(User);
        if (string.IsNullOrEmpty(userId))
            return Unauthorized();

        var user = await userRepository.Get(userId);
        if (user == null)
            return Forbid();

        var token = tokenService.Generate(user);
        return Ok(new LoginResponse
        {
            Token = token.token,
            ExpireUtc = token.expireUtc
        });
    }
}

 

Web Api로 프로젝트를 만들었을 때 자동으로 포함되는 Swagger에서 발급받은 토큰을 이용하여 로그인이 필요한 기능도 테스트할 수 있도록 builder.Services.AddSwaggerGen 메소드를 수정하였습니다.

builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Description = "Please enter a valid token",
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        BearerFormat = "JWT",
        Scheme = "Bearer"
    });
    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type= ReferenceType.SecurityScheme,
                    Id= "Bearer"
                }
            },
            new string[] {}
        }
    });
});

 

 

2. 유저정보 관리 구현

 

로그인 후 유저정보를 읽고, 쓸 때 필요한 기능들을 UserController 구현하였습니다.

  • Get
    • 클라이언트에서 유저정보를 불러올 때 사용
    • 처음 가입해서 보유한 차량이 없을 때 무료차량 1대를 지급한다.
  • SelectCar
    • 게임할 때 사용할 자동차 선택
  • BuyCar
    • 차량 구매
  • EndStage
    • 클라이언트에서 스테이지를 클리어했을 때 호출
    • 수익금을 지급하고, 다음 스테이지를 플레이할 수 있도록 DB 수정
[Route("[controller]")]
[ApiController]
public class UserController : ControllerBase
{
    readonly UserRepository userRepository;
    readonly TemplateService templateService;

    public UserController(UserRepository userRepository, TemplateService templateService)
    {
        this.userRepository = userRepository;
        this.templateService = templateService;
    }


    [HttpGet]
    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    public async Task<ActionResult> Get()
    {
        var userId = ClaimHelper.FindNameIdentifier(User);
        if (string.IsNullOrEmpty(userId))
            return Unauthorized();

        var user = await userRepository.Get(userId);
        if (user == null)
            return Forbid();

        // 자동차 지급
        if (user.Cars == null || user.Cars.Count == 0)
        {
            var cars = await templateService.GetCars();
            var newCar = cars.FirstOrDefault(e => e.Cost == 0);
            if (newCar != null)
            {
                user.CurrentCarId = newCar?.Id;
                user.Cars = [ user.CurrentCarId! ]; 
            }
        }

        return Ok(new
        {
            user.Nickname,
            user.Coin,
            user.Cars,
            CurrentCar = user.CurrentCarId,
            CurrentStage = user.CurrentStageIndex
        });
    }

    [HttpPut("SelctCar/{carId}")]
    public async Task<ActionResult> SelectCar(string carId)
    {
        var userId = ClaimHelper.FindNameIdentifier(User);
        if (string.IsNullOrEmpty(userId))
            return Unauthorized();

        var user = await userRepository.Get(userId);
        if (user == null)
            return Forbid();

        // 보유하지 않는 차는 선택 불가능
        var exist = user.Cars?.Contains(carId) ?? false;
        if (!exist)
            return NotFound();

        if (user.CurrentCarId != carId)
        {
            user.CurrentCarId = carId;
            await userRepository.Update(user.Id!, user);
        }

        return NoContent();
    }

    [HttpPut("BuyCar/{carId}")]
    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    public async Task<ActionResult> BuyCar(string carId)
    {
        var userId = ClaimHelper.FindNameIdentifier(User);
        if (string.IsNullOrEmpty(userId))
            return Unauthorized();

        var user = await userRepository.Get(userId);
        if (user == null)
            return Forbid();

        if (user.Cars == null)
            user.Cars = new();

        // 이미 보유한 차는 구매 불가능
        if (user.Cars.Contains(carId))
            return NoContent();

        var carTemps = await templateService.GetCars();
        var carTemp = carTemps.Find(e => e.Id == carId);
        if (carTemp == null)
            return NotFound();

        // 비용 부족
        if (user.Coin < carTemp.Cost)
            return Forbid();

        user.Coin -= carTemp.Cost;
        user.Cars.Add(carId);
        await userRepository.Update(user.Id!, user);

        return NoContent();
    }

    [HttpPut]
    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    public async Task<ActionResult> EndStage([FromBody] EndStageRequest body)
    {
        var userId = ClaimHelper.FindNameIdentifier(User);
        if (string.IsNullOrEmpty(userId))
            return Unauthorized();

        var user = await userRepository.Get(userId);
        if (user == null)
            return Forbid();

        var stageTemps = await templateService.GetStages();
        // 템플릿에 등록되지 않은 스테이지
        if (body.StageIndex < 0 || body.StageIndex >= stageTemps.Count)
            return BadRequest();

        // 현재 스테이지도 아직 클리어 하지 않았을 경우
        if (body.StageIndex > user.CurrentStageIndex)
            return Forbid();

        // 다음 스테이지 개방
        if (
            body.StageIndex == user.CurrentStageIndex && 
            user.CurrentStageIndex < stageTemps.Count - 1
        )
        {
            ++user.CurrentStageIndex;
        }

        // TODO: 나중에 보상금 검증하는 기능 추가
        user.Coin += body.Coin;
        await userRepository.Update(user.Id!, user);

        return NoContent();
    }
}

 

3. 요금 계산방식 수정

 

위에 방식에서는 클라이언트에서 계산한 요금이 제대로 된 금액이 맞는지 검증할 수 없어 최소한의 검사 기능을 추가해보기로 하였습니다.
요금 계산은 아래 설계한 흐름도처럼 해보기로 하였습니다.

 

요금 계산 공식도 서버에서 계산할 수 있도록 수정하였습니다.

  • m = d * r
    • m : 택시 요금(소수점 뒤는 버림)
    • d
      • 클라이언트 : 승객을 태우고 이동한 거리
      • 서버 : 총 이동거리(템플릿에 미리 입력)
      • r : 거리의 몇 %를 지급할지 설정하는 값(템플릿에 미리 입력)

실제 클라이언트는 모든 구간에서 손님을 태우지 않기 때문에 완벽한 처리방식은 아니지만 작업이 커질 것 같이 때문에 지금은 여기까지만 하고, 언젠가 수정하기로 마음먹었습니다.

유니티에서 콘솔창에 각각의 맵들의 이동경로 거리를 출력하는 테스트 코드를 추가하였습니다.

[ContextMenu("Print PlayerPath Distance")]
void PrintPlayerPathDistance()
{
    Debug.Log(playerPath.path.length);
}

 

 

거리를 확인한 후 스테이지 템플릿에 입력하였습니다.

 

template_generator.py 파일을 수정한 후 DB에 저장된 템플릿 정보를 변경하였습니다.

def generate_stage(file_path, sheet_name):
    book = load_workbook(file_path, data_only=True)
    sheet = book[sheet_name]
    skip = 0
    temp_group = []
    for row in sheet.rows:
        if skip < 1:
            skip += 1
            continue  
        if row[0].value == None:
            continue
        new_temp = {
            'Id': row[0].value,
            'Scene': row[1].value,
            'Distance': row[2].value,
            'FareRate': row[3].value
        }
        temp_group.append(new_temp)
    return temp_group

 

 

서버 프로젝트에서 StageTemplate, UserController를 수정하였습니다.

public class StageTemplate
{
    public string? Id { get; set; }
    public double Distance { get; set; }
    public double FareRate { get; set; }

    [JsonIgnore]
    public double MaxCoin => Distance * FareRate;
}
[HttpPut]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult> EndStage([FromBody] EndStageRequest body)
{
    var userId = ClaimHelper.FindNameIdentifier(User);
    if (string.IsNullOrEmpty(userId))
        return Unauthorized();

    var user = await userRepository.Get(userId);
    if (user == null)
        return Forbid();

    var stageTemps = await templateService.GetStages();
    // 템플릿에 등록되지 않은 스테이지
    if (body.StageIndex < 0 || body.StageIndex >= stageTemps.Count)
        return BadRequest();

    // 현재 스테이지도 아직 클리어하지 않았을 경우
    if (body.StageIndex > user.CurrentStageIndex)
        return Forbid();

    // 클라이언트에서 계산한 요금이 서버에서 계산한 요금보다 더 큰 경우
    if (body.Coin >= stageTemps[user.CurrentStageIndex].MaxCoin)
        return Forbid();

    // 다음 스테이지 개방
    if (
        body.StageIndex == user.CurrentStageIndex && 
        user.CurrentStageIndex < stageTemps.Count - 1
    )
    {
        ++user.CurrentStageIndex;
    }

    user.Coin += body.Coin;
    await userRepository.Update(user.Id!, user);

    return NoContent();
}

 

다음에는 다시 클라이언트 작업을 시작하여 게임을 완성하도록 하겠습니다.

깃 허브 저장소 : taxi-game-3d-unity

728x90
LIST
728x90
SMALL

1. 프로젝트 생성

서버는 아래 조건으로 만들기로 결정하였습니다.

  • 프로젝트 템플릿 : ASP.NET Core 웹 API
  • 프레임워크 : .NET 8.0
  • 데이터 베이스 : 몽고DB

프로젝트를 만든 후 NuGet 패키지 매니저에서 MongoDB.Driver를 추가하였습니다.

 

 

2. 서버 구현

유저정보를 관리할 때 사용할 UserModel 클래스와 UserRepository 클래스를 미리 만들어 두었습니다.

public class UserModel
{
    [BsonId]
    public string? Id { get; set; }
    public string? Nickname { get; set; }
    [BsonElement("Device")]
    [JsonPropertyName("Device")]
    public string? DeviceId { get; set; }
    public long Coin { get; set; }
    public List<string>? Cars { get; set; }
    [BsonElement("CurrentCar")]
    [JsonPropertyName("CurrentCar")]
    public string? CurrentCarId { get; set; }
    [BsonElement("CurrentStage")]
    [JsonPropertyName("CurrentStage")]
    public int CurrentStageIndex { get; set; }
}
public class UserRepository
{
    readonly IMongoCollection<UserModel> users;

    public UserRepository(DatabaseContext context)
    {
        users = context.Users;
    }

    public async Task Create(UserModel model) =>
        await users.InsertOneAsync(model);

    public async Task<UserModel> Get(string id) =>
        await users.Find(e => e.Id == id).FirstOrDefaultAsync();

    public async Task<UserModel> FindByDevice(string deviceId) =>
        await users.Find(e => e.DeviceId == deviceId).FirstOrDefaultAsync();

    public async Task Update(string id, UserModel model) =>
        await users.ReplaceOneAsync(e => e.Id == id, model);

    public async Task Delete(string id) =>
        await users.DeleteOneAsync(e => e.Id == id);
}

서버에서 DB에 템플릿 정보를 읽고, 쓰는데 사용할 모델 클래스들을 추가하였습니다.

public class TemplateVersionModel
{
    [BsonId]
    public string? Name { get; set; }
    public BsonArray? Datas { get; set; }
}
public class TemplateVersionModel
{
    [BsonId]
    public string? Name { get; set; }
    public ulong Version { get; set; }
}
  • TemplateModel
    • 템플릿 데이터 관리
    • 실제 개발자가 업로드한 데이터들
  • TemplateVersionModel
    • 템플릿의 버전을 관리
    • 개발자가 DB에 템플릿을 업로드할 때마다 버전을 증가 시킨다.
    • 클라이언트에서는 템플릿을 버전을 비교하여 새로 받을지 말지 결정한다.

서버에서 처음 가입한 유저에게 차량 1대를 주거나 자동차의 가격과 유저가 가진 금액을 비교할 때 템플릿에 저장된 정보가 필요합니다.
그렇다고 클라이언트에서 요청을 할 때마다 서버에서 계속 DB와 통신을 하는 것은 좋지 않다고 생각하여 서버에서 템플릿 정보를 메모리에 따로 저장하고 있도록 구현하기로 하였습니다.
그래서 템플릿을 메모리에서 관리할 때 필요한 클래스들을 추가하였습니다.

public class CarTemplate
{
    public string? Id { get; set; }
    public int Cost { get; set; }
}
public class StageTemplate
{
    public string? Id { get; set; }
}
public class TemplateService
{
    readonly IMongoCollection<TemplateVersionModel> versions;
    readonly IMongoCollection<TemplateModel> templates;

    List<CarTemplate>? cars;
    List<StageTemplate>? stages;

    public TemplateService(DatabaseContext context)
    {
        versions = context.TemplateVersions;
        templates = context.Templates;
    }

    public async Task<ulong> Update(string name, JsonArray datas)
    {
        switch (name)
        {
            case "Car":
                cars = datas.Deserialize<List<CarTemplate>>();
                break;
            case "Stage":
                stages = datas.Deserialize<List<StageTemplate>>();
                break;
            default:
                return 0;
        }

        var verModel = await versions.Find(e => e.Name == name).FirstOrDefaultAsync();
        if (verModel != null)
        {
            ++verModel.Version;
            await versions.ReplaceOneAsync(e => e.Name == name, verModel);
            await templates.ReplaceOneAsync(e => e.Name == name, new()
            {
                Name = name,
                Datas = BsonSerializer.Deserialize<BsonArray>(datas.ToJsonString())
            });
            return verModel.Version;
        }
        else
        {
            await versions.InsertOneAsync(new()
            {
                Name = name,
                Version = 1
            });
            await templates.InsertOneAsync(new()
            {
                Name = name,
                Datas = BsonSerializer.Deserialize<BsonArray>(datas.ToJsonString())
            });
            return 1;
        }
    }

    public async Task Delete(string name)
    {
        await versions.DeleteOneAsync(e => e.Name == name);
        await templates.DeleteOneAsync(e => e.Name == name);
    }

    public async Task<Dictionary<string, ulong>> GetVersions()
    {
        var models = await versions.Find(e => true).ToListAsync();
        var result = new Dictionary<string, ulong>();
        foreach (var e in models)
        {
            if (!string.IsNullOrEmpty(e.Name))
                result.Add(e.Name, e.Version);
        }
        return result;
    }

    public async Task<JsonArray?> Get(string name)
    {
        var model = await templates.Find(e => e.Name == name).FirstOrDefaultAsync();
        if (model == null)
            return null;
        return JsonSerializer.Deserialize<JsonArray>(model.Datas.ToJson());
    }

    public async Task<List<CarTemplate>> GetCars()
    {
        if (cars == null)
        {
            var model = await templates.Find(e => e.Name == "Car").FirstOrDefaultAsync();
            cars = BsonSerializer.Deserialize<List<CarTemplate>>(model.Datas.ToJson());
        }
        return cars;
    }

    public async Task<List<StageTemplate>> GetStages()
    {
        if (stages == null)
        {
            var model = await templates.Find(e => e.Name == "Stage").FirstOrDefaultAsync();
            stages = BsonSerializer.Deserialize<List<StageTemplate>>(model.Datas.ToJson());
        }
        return stages;
    }
}

 

서버에서 프리팹과 씬정보를 알고 있을 필요가 없기 때문에 CarTemplate 클래스와 StageTemplate 클래스는 서버에 필요없는 정보들은 정의하지 않았습니다.
TemplateService 클래스는 UserRepository 처럼 UserModel 하나만 관리하지 않고, 상대적으로 다양한 기능을 가지고 있기 때문에 Repository라고 작명하지 않았습니다.

클라이언트에서 서버와 통신할 수 있도록 TemplateController 클래스도 추가하였습니다.

[Route("[controller]")]
[ApiController]
public class TemplateController : ControllerBase
{
    readonly TemplateService service;

    public TemplateController(TemplateService service)
    {
        this.service = service;
    }

    [HttpGet("versions")]
    public async Task<ActionResult> GetVersions()
    {
        var versions = await service.GetVersions();
        return Ok(versions);
    }

    [HttpGet("{name}")]
    public async Task<ActionResult> Get(string name)
    {
        var templates = await service.Get(name);
        if (templates == null)
            return NotFound();
        return Ok(templates);
    }

    [HttpPut("{name}")]
    public async Task<ActionResult> Update(string name, [FromBody] JsonArray datas)
    {
        var version = await service.Update(name, datas);
        return Ok(new { Version = version } );
    }

    [HttpDelete("{name}")]
    public async Task<ActionResult> Delete(string name)
    {
        await service.Delete(name);
        return NoContent();
    }
}

 

옛날에는 게임과 서버가 통신할 때 POST만 사용하도록 구현하였지만 이번에는 GET, PUT, DELETE도 함께 사용해 보려고 합니다.
JSON도 여태까지 Netwonsoft.Json을 사용해 왔지만 요즘은 MS에서 제공하는 JSON이 더 빠르다는 내용을 인터넷에서 확인하여 Netwonsoft.Json을 대신 사용하도록 설정하는 작업은 하지 않았습니다.

 

3. 템플릿 연동

파이썬을 이용하여 xlsx에 입력한 정보를 서버와 DB에 저장하는 기능을 구현하였습니다.

template_generator.py 를 만들어 xlsx 파일에 입력한 데이터를 딕셔너리로 변환하는 코드를 정리하였습니다.

from openpyxl import load_workbook

def generate_car(file_path, sheet_name):
    book = load_workbook(file_path, data_only=True)
    sheet = book[sheet_name]
    skip = 0
    temp_group = []
    for row in sheet.rows:
        if skip < 2:
            skip += 1
            continue      
        if row[0].value == None:
            continue
        new_temp = {
            'Id': row[0].value,
            'Name': {
                'Table': row[1].value,
                'Key': row[2].value
            },
            'Icon': row[3].value,
            'Prefab': row[4].value,
            'Cost': row[5].value
        }
        temp_group.append(new_temp)
    return temp_group

def generate_stage(file_path, sheet_name):
    book = load_workbook(file_path, data_only=True)
    sheet = book[sheet_name]
    skip = 0
    temp_group = []
    for row in sheet.rows:
        # 두번째 줄부터 데이터 저장
        if skip < 1:
            skip += 1
            continue  
        # ID컬럼이 비어 있으면 추가하지 않음
        if row[0].value == None:
            continue
        new_temp = {
            'Id': row[0].value,
            'Scene': row[1].value
        }
        temp_group.append(new_temp)
    return temp_group

 

deploy-car-client.py, deploy-stage-client.pytemplate_generator.py 에서 변환한 데이터를 저장하도록 수정하였습니다.

import json
from template_generator import generate_stage 

temp_group = generate_stage('./Template(Dev).xlsx', 'Car')
client_path = '../taxi-game-3d-client/Assets/_TaxiGame/Resources/Templates/Car.json'
with open(client_path, 'w', encoding='UTF-8-sig') as f:
    json.dump(temp_group, f, ensure_ascii=False)
import json
from template_generator import generate_stage

temp_group = generate_stage('./Template(Dev).xlsx', 'Stage') 
client_path = '../taxi-game-3d-client/Assets/_TaxiGame/Resources/Templates/Stage.json'
with open(client_path, 'w', encoding='UTF-8-sig') as f:
    json.dump(temp_group, f, ensure_ascii=False)

 

deploy-car-server.py, deploy-stage-client.py 추가하여 서버에 변환된 데이터를 전달하는 기능을 구현하였습니다.

import requests
from template_generator import generate_car

temp_group = generate_car('./Template(Dev).xlsx', 'Car')
res = requests.put('https://localhost:7170/Template/Car', json=temp_group, verify=False)
print(res.status_code)
import requests
from template_generator import generate_stage

temp_group = generate_stage('./Template(Dev).xlsx', 'Stage')
res = requests.put('https://localhost:7170/Template/Stage', json=temp_group, verify=False)
print(res.status_code)

 

여러 번의 테스트와 수정을 통해 DB에 잘 저장된 것을 확인 하였습니다.

 

이번 작업은 아직 파이썬, MS에서 기본 제공하는 JSON, 몽고DB가 익숙하지 않아 삽질을 많이 했습니다.
다음에는 로그인, 로그아웃, 유저정보관리 등 기능을 구현하겠습니다.

 

깃 허브 저장소 : taxi-game-3d-unity

728x90
LIST

+ Recent posts